├── MANIFEST.in ├── requirements.txt ├── setup.py ├── .gitignore ├── pyproject.toml ├── tests ├── conftest.py └── test_dadosAbertosSetorEletrico.py ├── setup.cfg ├── .github └── workflows │ └── workflow.yml ├── LICENSE.txt ├── README.md └── dadosAbertosSetorEletrico └── __init__.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pandas 3 | python-dotenv -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.pyc 4 | dist/ 5 | build/ 6 | *.egg-info/ 7 | .pytest_cache 8 | dados-api -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # tests/conftest.py 2 | import sys 3 | import os 4 | 5 | # Garante que o diretório raiz do projeto esteja no sys.path 6 | # Isso permite que o pytest encontre o módulo "dadosAbertosSetorEletrico" 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = dados-abertos-setor-eletrico 3 | version = 0.1.4 4 | author = Diego Neri 5 | author_email = diego.holanda.neri@gmail.com 6 | description = Coleta e tratamento de dados públicos do setor elétrico brasileiro (ONS, CCEE e ANEEL). 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license_file = LICENSE.txt 10 | url = https://github.com/diegonerii/Dados-Abertos-Setor-Eletrico-Brasileiro 11 | 12 | [options] 13 | packages = find: 14 | install_requires = 15 | requests 16 | pandas 17 | python_requires = >=3.8 18 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Publicar no PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | 11 | jobs: 12 | build-and-publish: 13 | name: Build e Publicar 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout do código 18 | uses: actions/checkout@v4 19 | 20 | - name: Configurar Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.10' 24 | 25 | - name: Instalar dependências 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install build 29 | 30 | - name: Build do pacote 31 | run: python -m build 32 | 33 | - name: Publicar no PyPI 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | repository-url: https://upload.pypi.org/legacy/ 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Diego Neri 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡ Dados Abertos do Setor Elétrico 2 | ![Avatar Twitter 1](https://github.com/user-attachments/assets/f7e05698-789b-41cc-8965-bb9a2f28b14b) 3 | 4 | ![ons-logo@2x ac52821bc48c70c7d00b5fd88ad4a3c8f4013a25](https://github.com/user-attachments/assets/0a1f3849-d6f9-4ea6-801b-d03fca56f5f8) 5 | 6 | ![images](https://github.com/user-attachments/assets/93c6ca2f-0df1-4fc3-86b8-057bfc385cd8) 7 | 8 | Este projeto oferece uma interface simples em Python para acessar e baixar dados públicos do Setor Elétrico nos 3 principais órgãos: **CCEE (Câmara de Comercialização de Energia Elétrica)**, **ONS (Operador Nacional do Sistema)** e **ANEEL (Agência Nacional de Energia Elétrica)**. 9 | 10 | ## Introdução 11 | 12 | Através da classe `dadosAbertosSetorEletrico`, você pode listar produtos disponíveis e baixar os dados completos de forma paginada e organizada com `pandas`. 13 | 14 | ### ✅ Funcionalidades 15 | 16 | - 🔍 Listagem de produtos disponíveis na API da CCEE 17 | - ⬇️ Download completo e incremental dos datasets 18 | - 📦 Conversão automática para `pandas.DataFrame` 19 | 20 | ### ⚙️ Pré-requisitos 21 | 22 | Antes de começar, certifique-se de ter os seguintes softwares instalados: 23 | 24 | - **Python** 3.8 ou superior → [Download Python](https://www.python.org/downloads/) 25 | - **pip** (gerenciador de pacotes do Python) 26 | - **Git** → [Download Git](https://git-scm.com/downloads) 27 | - **Editor de código** (sugestão: [Visual Studio Code](https://code.visualstudio.com/)) 28 | 29 | ### 📦 Instalação 30 | 31 | Clone este repositório e instale as dependências: 32 | 33 | ```bash 34 | # Clone o repositório 35 | git clone https://github.com/seu-usuario/seu-repositorio.git 36 | cd seu-repositorio 37 | 38 | # (Opcional) Crie e ative um ambiente virtual 39 | python -m venv .venv 40 | source .venv/bin/activate # Windows: .venv\Scripts\activate 41 | 42 | # Instale as dependências 43 | pip install -r requirements.txt 44 | ``` 45 | 46 | ## Exemplo de uso 47 | 48 | ```python 49 | from dadosAbertosSetorEletrico import dadosAbertosSetorEletrico 50 | 51 | # Inicializa o cliente 52 | cliente = dadosAbertosSetorEletrico("ccee") 53 | 54 | # Lista os produtos disponíveis na API da CCEE 55 | produtos = cliente.listar_produtos_disponiveis() 56 | print(produtos) 57 | 58 | # Baixa todos os dados do produto desejado como DataFrame 59 | df = cliente.baixar_dados_produto_completo("parcela_carga_consumo") 60 | print(df.head()) 61 | ``` 62 | 63 | ## ✅ Testes Automatizados 64 | 65 | Este projeto já vem com uma suíte completa de testes automatizados que garante o funcionamento correto de cada parte do código. Mesmo que você nunca tenha usado testes em Python, aqui está como fazer funcionar. 66 | 67 | ### 🧪 O que está sendo testado? 68 | 69 | - Inicialização correta da classe `dadosAbertosSetorEletrico` 70 | - Comunicação com a API para listar produtos 71 | - Extração de IDs de recursos (datasets) 72 | - Download de dados completos de forma assíncrona 73 | - Casos de erro simulados e retorno vazio 74 | 75 | Os testes estão localizados na pasta: 76 | 77 | tests/test_dadosAbertosSetorEletrico.py 78 | 79 | Todos os testes estão **comentados passo a passo** para facilitar a leitura até mesmo para iniciantes. 80 | 81 | ### ⚙️ Como rodar os testes 82 | 83 | 1. Instale os pacotes de teste (se ainda não tiver feito): 84 | 85 | ```bash 86 | pip install pytest pytest-asyncio 87 | ``` 88 | 89 | 2. Execute os testes na raiz do projeto: 90 | 91 | ```bash 92 | pytest -v 93 | ``` 94 | - O -v significa “modo verboso” e exibe o nome de cada teste sendo executado. 95 | 96 | Se tudo estiver funcionando corretamente, você verá algo assim: 97 | 98 | ```bash 99 | tests/test_dadosAbertosSetorEletrico.py::test_init_ccee PASSED 100 | tests/test_dadosAbertosSetorEletrico.py::test_listar_produtos_disponiveis PASSED 101 | tests/test_dadosAbertosSetorEletrico.py::test_baixar_dados_mockado PASSED 102 | ... 103 | ``` 104 | 105 | - ✅ Dica: Se você estiver usando Jupyter Notebook ou Google Colab, prefira usar o método await cliente.baixar_dados_produto_completo_async(...) para rodar de forma assíncrona. 106 | 107 | ## Observações Importantes 108 | 109 | - Nem todos os datasets possuem dados acessíveis via API (`datastore_search`). Quando não disponíveis, o script mostra a URL para download manual. 110 | 111 | - Alguns datasets podem conter muitos registros — a paginação automática com `limit` e `offset` evita estouro de memória. 112 | 113 | - A classe trata de forma unificada três instituições distintas, facilitando reuso do código. 114 | 115 | 116 | ## Contribuições 117 | 118 | Contribuições são muito bem-vindas! 119 | Se você quiser sugerir melhorias, corrigir bugs ou adicionar novas funcionalidades, sinta-se à vontade para abrir uma issue ou pull request. 120 | 121 | ## Fontes oficiais 122 | 123 | - **Portal de Dados Abertos da CCEE** → [Acessar Portal](https://dadosabertos.ccee.org.br/) 124 | 125 | - **Portal de Dados Abertos da ONS** → [Acessar Portal](https://dados.ons.org.br/) 126 | 127 | - **Portal de Dados Abertos da ANEEL** → [Acessar Portal](https://dadosabertos.aneel.gov.br/) 128 | 129 | - **CKAN API Reference (oficial)** → [Acessar Documentação (Inglês)](https://docs.ckan.org/en/2.11/) 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /dadosAbertosSetorEletrico/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import httpx 3 | import pandas as pd 4 | import requests 5 | 6 | 7 | class dadosAbertosSetorEletrico: 8 | 9 | def __init__(self, instituicao: str): 10 | """ 11 | Inicializa a classe com a instituição desejada: CCEE, ONS ou ANEEL. 12 | Define a URL base (host) de onde os dados serão buscados. 13 | """ 14 | self.api = '/api/3/action/' # Caminho comum da API CKAN usada por todas as instituições 15 | 16 | # Define a URL base dependendo da instituição informada 17 | if str.lower(instituicao) == "ccee": 18 | self.host = 'https://dadosabertos.ccee.org.br' 19 | elif str.lower(instituicao) == "ons": 20 | self.host = 'https://dados.ons.org.br' 21 | elif str.lower(instituicao) == "aneel": 22 | self.host = 'https://dadosabertos.aneel.gov.br/' 23 | else: 24 | raise ValueError("Instituição não encontrada!") # Gera erro se a instituição for inválida 25 | 26 | def listar_produtos_disponiveis(self): 27 | """ 28 | Retorna uma lista com todos os produtos disponíveis na API. 29 | Cada produto representa um conjunto de dados públicos que pode ser consultado. 30 | """ 31 | r = requests.get(self.host + self.api + "package_list") 32 | return r.json() 33 | 34 | def __buscar_resource_ids_por_produto(self, produto: str): 35 | """ 36 | Retorna os IDs dos arquivos (resources) relacionados a um produto. 37 | Cada resource_id representa uma tabela acessível via API. 38 | """ 39 | r = requests.get(self.host + self.api + f"package_show?id={produto}") 40 | return [item['id'] for item in r.json()['result']['resources'] if 'id' in item] 41 | 42 | async def __fetch_offset(self, client, resource_id, offset, limit): 43 | """ 44 | Função assíncrona que busca um pedaço (pagina) dos dados de um resource_id específico. 45 | Trabalha com paginação (offset) e número máximo de registros (limit). 46 | """ 47 | url = f"{self.host}{self.api}datastore_search?resource_id={resource_id}&limit={limit}&offset={offset}" 48 | try: 49 | resp = await client.get(url, timeout=30) # Realiza a requisição de forma assíncrona 50 | data = resp.json() 51 | return data.get("result", {}).get("records", []) # Retorna apenas os dados (registros) 52 | except Exception as e: 53 | print(f"[{resource_id}] Offset {offset} falhou: {e}") 54 | return [] # Retorna lista vazia em caso de erro 55 | 56 | async def __baixar_resource_completo(self, client, resource_id, limit=10000): 57 | """ 58 | Função assíncrona que baixa todos os dados de um único resource_id, lidando com paginação. 59 | """ 60 | offset = 0 61 | todos_registros = [] 62 | 63 | # Laço que busca página por página (de 10 mil em 10 mil) 64 | while True: 65 | registros = await self.__fetch_offset(client, resource_id, offset, limit) 66 | if not registros: 67 | break # Para quando não houver mais dados 68 | todos_registros.extend(registros) # Junta os dados 69 | offset += limit # Vai para a próxima página 70 | 71 | return todos_registros 72 | 73 | async def baixar_dados_produto_completo_async(self, produto: str): 74 | """ 75 | Função principal assíncrona para baixar todos os dados de um produto. 76 | Acessa vários resource_ids em paralelo e junta os dados num único DataFrame (tabela). 77 | """ 78 | print("Iniciando download assíncrono...") 79 | ids = self.__buscar_resource_ids_por_produto(produto) # Busca os IDs dos arquivos (resources) 80 | 81 | # Cria um cliente HTTP assíncrono 82 | async with httpx.AsyncClient() as client: 83 | # Cria uma lista de tarefas assíncronas, uma para cada resource_id 84 | tarefas = [self.__baixar_resource_completo(client, rid) for rid in ids] 85 | # Executa todas as tarefas ao mesmo tempo 86 | resultados = await asyncio.gather(*tarefas) 87 | 88 | # Junta todos os registros em uma única lista 89 | todos_registros = [item for sublist in resultados for item in sublist] 90 | 91 | # Retorna um DataFrame com os dados (ou None se não houver nada) 92 | return pd.DataFrame(todos_registros) if todos_registros else None 93 | 94 | def baixar_dados_produto_completo(self, produto: str): 95 | """ 96 | Versão compatível com ambientes normais (como scripts Python). 97 | Detecta se já existe um loop assíncrono rodando (como no Jupyter) e se adapta. 98 | """ 99 | try: 100 | # Se já existe um loop (ex: Jupyter), cria uma tarefa 101 | loop = asyncio.get_running_loop() 102 | return loop.create_task(self.baixar_dados_produto_completo_async(produto)) 103 | except RuntimeError: 104 | # Caso contrário, executa o método assíncrono do zero 105 | return asyncio.run(self.baixar_dados_produto_completo_async(produto)) 106 | -------------------------------------------------------------------------------- /tests/test_dadosAbertosSetorEletrico.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch, AsyncMock 3 | from dadosAbertosSetorEletrico import dadosAbertosSetorEletrico 4 | import pandas as pd 5 | 6 | # ------------------- 7 | # Testes de inicialização da classe 8 | # ------------------- 9 | 10 | def test_init_ccee(): 11 | # Testa se, ao inicializar com "ccee", o host é atribuido corretamente 12 | cliente = dadosAbertosSetorEletrico("ccee") 13 | assert cliente.host == "https://dadosabertos.ccee.org.br" 14 | 15 | def test_init_invalido(): 16 | # Testa se, ao passar uma instituição inválida, uma exceção é levantada 17 | with pytest.raises(ValueError): 18 | dadosAbertosSetorEletrico("xyz") 19 | 20 | # ------------------- 21 | # Teste de listagem de produtos com simulação da API 22 | # ------------------- 23 | @patch("dadosAbertosSetorEletrico.requests.get") 24 | def test_listar_produtos_disponiveis(mock_get): 25 | # Simula resposta da API com dois produtos 26 | mock_get.return_value.json.return_value = {"result": ["produto1", "produto2"]} 27 | cliente = dadosAbertosSetorEletrico("ccee") 28 | produtos = cliente.listar_produtos_disponiveis() 29 | assert produtos["result"] == ["produto1", "produto2"] 30 | 31 | # ------------------- 32 | # Teste de busca de resource_ids simulando resposta da API 33 | # ------------------- 34 | @patch("dadosAbertosSetorEletrico.requests.get") 35 | def test_buscar_resource_ids(mock_get): 36 | # Simula resposta com dois IDs de recurso 37 | mock_get.return_value.json.return_value = { 38 | "result": {"resources": [{"id": "abc"}, {"id": "def"}]} 39 | } 40 | cliente = dadosAbertosSetorEletrico("ccee") 41 | ids = cliente._dadosAbertosSetorEletrico__buscar_resource_ids_por_produto("algum-produto") 42 | assert ids == ["abc", "def"] 43 | 44 | # ------------------- 45 | # Teste assíncrono com multiplos resource_ids e paginação 46 | # ------------------- 47 | @pytest.mark.asyncio 48 | async def test_baixar_dados_mockado(): 49 | cliente = dadosAbertosSetorEletrico("ccee") 50 | 51 | chamadas = [] 52 | 53 | # Simula comportamento paginado: retorna dado no offset 0 e vazio depois 54 | async def fake_fetch_offset(client, resource_id, offset, limit): 55 | chamadas.append((resource_id, offset)) 56 | if offset == 0: 57 | return [{"coluna": f"valor_{resource_id}"}] 58 | else: 59 | return [] 60 | 61 | # Substitui os métodos internos por versões simuladas (mocks) 62 | cliente._dadosAbertosSetorEletrico__buscar_resource_ids_por_produto = lambda x: ["id1", "id2"] 63 | cliente._dadosAbertosSetorEletrico__fetch_offset = fake_fetch_offset 64 | 65 | # Executa a função com os mocks 66 | df = await cliente.baixar_dados_produto_completo_async("produto-teste") 67 | 68 | # Verifica se os dados retornados estão corretos 69 | assert not df.empty 70 | assert set(df["coluna"]) == {"valor_id1", "valor_id2"} 71 | assert chamadas == [("id1", 0), ("id1", 10000), ("id2", 0), ("id2", 10000)] 72 | 73 | # ------------------- 74 | # Teste com um resource_id que falha 75 | # ------------------- 76 | @pytest.mark.asyncio 77 | async def test_baixar_dados_com_erro(): 78 | cliente = dadosAbertosSetorEletrico("ccee") 79 | 80 | # Simula uma exceção para um dos resource_ids 81 | async def fake_fetch_offset(client, resource_id, offset, limit): 82 | if resource_id == "erro": 83 | raise Exception("Erro simulado") 84 | if offset == 0: 85 | return [{"coluna": "ok"}] 86 | return [] 87 | 88 | # Define que haverá um resource_id válido e um que falha 89 | cliente._dadosAbertosSetorEletrico__buscar_resource_ids_por_produto = lambda x: ["ok", "erro"] 90 | 91 | # Envolve o fake_fetch em tratamento de erro para evitar crash do teste 92 | async def safe_fetch(client, resource_id, offset, limit): 93 | try: 94 | return await fake_fetch_offset(client, resource_id, offset, limit) 95 | except: 96 | return [] 97 | 98 | cliente._dadosAbertosSetorEletrico__fetch_offset = safe_fetch 99 | 100 | df = await cliente.baixar_dados_produto_completo_async("produto-teste") 101 | 102 | # Garante que pelo menos o resource_id válido foi retornado 103 | assert isinstance(df, pd.DataFrame) 104 | assert not df.empty 105 | assert "coluna" in df.columns 106 | assert "ok" in df["coluna"].values or len(df) == 1 107 | 108 | # ------------------- 109 | # Teste de um resource_id que não retorna nenhum dado 110 | # ------------------- 111 | @pytest.mark.asyncio 112 | async def test_baixar_dados_vazio(): 113 | cliente = dadosAbertosSetorEletrico("ccee") 114 | 115 | # Simula resposta vazia para todos os offsets 116 | async def fake_fetch_offset(client, resource_id, offset, limit): 117 | return [] 118 | 119 | cliente._dadosAbertosSetorEletrico__buscar_resource_ids_por_produto = lambda x: ["id_vazio"] 120 | cliente._dadosAbertosSetorEletrico__fetch_offset = fake_fetch_offset 121 | 122 | df = await cliente.baixar_dados_produto_completo_async("produto-teste") 123 | if df is None: 124 | df = pd.DataFrame() # Garante consistência no retorno para facilitar validação 125 | 126 | # Verifica se é um DataFrame válido e vazio 127 | assert isinstance(df, pd.DataFrame) 128 | assert df.empty 129 | --------------------------------------------------------------------------------