nia-etl-utils 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ """Módulo utilitário para configuração de logging com Loguru."""
2
+ import sys
3
+ from pathlib import Path
4
+ from loguru import logger
5
+
6
+
7
+ def configurar_logger(
8
+ prefixo: str,
9
+ data_extracao: str,
10
+ pasta_logs: str = "logs",
11
+ rotation: str = "10 MB",
12
+ retention: str = "7 days",
13
+ level: str = "DEBUG"
14
+ ) -> str:
15
+ """Configura o logger da aplicação com Loguru.
16
+
17
+ Cria um handler de arquivo para o logger com rotação automática e retenção
18
+ configurável. O arquivo de log é criado em uma estrutura de diretórios
19
+ organizada por prefixo.
20
+
21
+ Args:
22
+ prefixo: Nome do módulo/pipeline (ex: 'extract', 'transform', 'load').
23
+ data_extracao: Data usada no nome do arquivo de log (ex: '2025_01_19').
24
+ pasta_logs: Diretório raiz onde os logs serão armazenados. Defaults to "logs".
25
+ rotation: Critério de rotação do arquivo (tamanho ou tempo). Defaults to "10 MB".
26
+ retention: Tempo de retenção dos logs antigos. Defaults to "7 days".
27
+ level: Nível mínimo de log a ser registrado. Defaults to "DEBUG".
28
+
29
+ Returns:
30
+ str: Caminho completo do arquivo de log criado.
31
+
32
+ Raises:
33
+ SystemExit: Se houver erro ao criar diretórios ou configurar o logger.
34
+
35
+ Examples:
36
+ >>> from nia_etl_utils.logger_config import configurar_logger
37
+ >>> caminho_log = configurar_logger("extract", "2025_01_19")
38
+ >>> logger.info("Pipeline iniciado")
39
+ >>> # Log salvo em: logs/extract/extract_2025_01_19.log
40
+
41
+ >>> # Com configurações customizadas
42
+ >>> caminho_log = configurar_logger(
43
+ ... prefixo="etl_ouvidorias",
44
+ ... data_extracao="2025_01_19",
45
+ ... pasta_logs="/var/logs/nia",
46
+ ... rotation="50 MB",
47
+ ... retention="30 days",
48
+ ... level="INFO"
49
+ ... )
50
+ """
51
+ try:
52
+ # Valida inputs
53
+ if not prefixo or not prefixo.strip():
54
+ logger.error("Prefixo não pode ser vazio.")
55
+ sys.exit(1)
56
+
57
+ if not data_extracao or not data_extracao.strip():
58
+ logger.error("Data de extração não pode ser vazia.")
59
+ sys.exit(1)
60
+
61
+ # Cria estrutura de diretórios
62
+ diretorio_log = Path(pasta_logs) / prefixo
63
+ diretorio_log.mkdir(parents=True, exist_ok=True)
64
+
65
+ # Define caminho do arquivo de log
66
+ caminho_log = diretorio_log / f"{prefixo}_{data_extracao}.log"
67
+
68
+ # Configura handler do Loguru
69
+ logger.add(
70
+ str(caminho_log),
71
+ rotation=rotation,
72
+ retention=retention,
73
+ level=level,
74
+ encoding="utf-8",
75
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}"
76
+ )
77
+
78
+ logger.info(f"Logger configurado com sucesso. Arquivo de log: {caminho_log}")
79
+
80
+ return str(caminho_log)
81
+
82
+ except PermissionError as error:
83
+ logger.error(f"Sem permissão para criar diretório de logs '{pasta_logs}': {error}")
84
+ sys.exit(1)
85
+ except OSError as error:
86
+ logger.error(f"Erro do sistema ao configurar logger em '{pasta_logs}': {error}")
87
+ sys.exit(1)
88
+ except Exception as error:
89
+ logger.error(f"Erro inesperado ao configurar logger: {error}")
90
+ sys.exit(1)
91
+
92
+
93
+ def configurar_logger_padrao_nia(nome_pipeline: str) -> str:
94
+ """Configura logger com padrões do NIA para pipelines de produção.
95
+
96
+ Esta é uma função de conveniência que aplica as configurações padrão
97
+ usadas pelos pipelines ETL do NIA:
98
+ - Rotação: 50 MB
99
+ - Retenção: 30 dias
100
+ - Nível: INFO (menos verboso que DEBUG)
101
+ - Pasta: logs/ (relativa ao diretório de execução)
102
+
103
+ Args:
104
+ nome_pipeline: Nome do pipeline (será usado como prefixo e na data).
105
+
106
+ Returns:
107
+ str: Caminho completo do arquivo de log criado.
108
+
109
+ Examples:
110
+ >>> from nia_etl_utils.logger_config import configurar_logger_padrao_nia
111
+ >>> from datetime import datetime
112
+ >>>
113
+ >>> caminho_log = configurar_logger_padrao_nia("ouvidorias_etl")
114
+ >>> logger.info("Pipeline iniciado com configurações padrão NIA")
115
+ """
116
+ from datetime import datetime
117
+
118
+ data_hoje = datetime.now().strftime("%Y_%m_%d")
119
+
120
+ return configurar_logger(
121
+ prefixo=nome_pipeline,
122
+ data_extracao=data_hoje,
123
+ pasta_logs="logs",
124
+ rotation="50 MB",
125
+ retention="30 days",
126
+ level="INFO"
127
+ )
128
+
129
+
130
+ def remover_handlers_existentes() -> None:
131
+ """Remove todos os handlers existentes do logger.
132
+
133
+ Útil quando você precisa reconfigurar o logger do zero ou quando está
134
+ rodando múltiplos scripts em sequência que configuram o logger.
135
+
136
+ Examples:
137
+ >>> from nia_etl_utils.logger_config import remover_handlers_existentes, configurar_logger
138
+ >>>
139
+ >>> # Remove handlers anteriores
140
+ >>> remover_handlers_existentes()
141
+ >>>
142
+ >>> # Configura novo logger
143
+ >>> configurar_logger("novo_pipeline", "2025_01_19")
144
+ """
145
+ logger.remove()
146
+ logger.info("Todos os handlers do logger foram removidos.")
@@ -0,0 +1,209 @@
1
+ """Módulo para exportação de DataFrame para CSV.
2
+
3
+ Fornece funções utilitárias para salvar DataFrames em formato CSV com
4
+ nomenclatura padronizada e logging adequado.
5
+ """
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Callable, Optional
9
+ import pandas as pd
10
+ from loguru import logger
11
+
12
+
13
+ def exportar_para_csv(
14
+ df: pd.DataFrame,
15
+ nome_arquivo: str,
16
+ data_extracao: str,
17
+ diretorio_base: str
18
+ ) -> str:
19
+ """Salva um DataFrame como arquivo CSV.
20
+
21
+ Args:
22
+ df: DataFrame a ser salvo.
23
+ nome_arquivo: Nome base do arquivo (sem extensão).
24
+ data_extracao: Data que será usada no nome do arquivo (ex: "2025_01_19").
25
+ diretorio_base: Diretório onde o arquivo será salvo.
26
+
27
+ Returns:
28
+ str: Caminho completo do arquivo salvo.
29
+
30
+ Raises:
31
+ SystemExit: Se houver erro ao salvar o arquivo.
32
+
33
+ Examples:
34
+ >>> import pandas as pd
35
+ >>> from nia_etl_utils.processa_csv import exportar_para_csv
36
+ >>>
37
+ >>> df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
38
+ >>> caminho = exportar_para_csv(df, "dados", "2025_01_19", "/tmp")
39
+ >>> # Arquivo salvo: /tmp/dados_2025_01_19.csv
40
+ """
41
+ try:
42
+ # Valida inputs
43
+ if df is None or df.empty:
44
+ logger.warning("DataFrame vazio ou None fornecido. Nenhum arquivo será criado.")
45
+ return ""
46
+
47
+ if not nome_arquivo or not nome_arquivo.strip():
48
+ logger.error("Nome do arquivo não pode ser vazio.")
49
+ sys.exit(1)
50
+
51
+ # Cria diretório se não existir
52
+ diretorio = Path(diretorio_base)
53
+ diretorio.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Monta caminho completo
56
+ caminho_arquivo = diretorio / f"{nome_arquivo}_{data_extracao}.csv"
57
+
58
+ # Salva CSV
59
+ df.to_csv(caminho_arquivo, index=False, encoding='utf-8')
60
+
61
+ # Log com informações úteis
62
+ tamanho_kb = caminho_arquivo.stat().st_size / 1024
63
+ logger.success(
64
+ f"CSV salvo: {caminho_arquivo} "
65
+ f"({len(df)} linhas, {len(df.columns)} colunas, {tamanho_kb:.2f} KB)"
66
+ )
67
+
68
+ return str(caminho_arquivo)
69
+
70
+ except PermissionError as error:
71
+ logger.error(f"Sem permissão para salvar arquivo em '{diretorio_base}': {error}")
72
+ sys.exit(1)
73
+ except OSError as error:
74
+ logger.error(f"Erro do sistema ao salvar CSV em '{diretorio_base}': {error}")
75
+ sys.exit(1)
76
+ except Exception as error:
77
+ logger.error(f"Erro inesperado ao salvar CSV: {error}")
78
+ sys.exit(1)
79
+
80
+
81
+ def extrair_e_exportar_csv(
82
+ nome_extracao: str,
83
+ funcao_extracao: Callable[[], pd.DataFrame],
84
+ data_extracao: str,
85
+ diretorio_base: str,
86
+ falhar_se_vazio: bool = False
87
+ ) -> Optional[str]:
88
+ """Executa uma função de extração e salva o resultado como CSV.
89
+
90
+ Args:
91
+ nome_extracao: Nome base para o arquivo CSV (sem extensão).
92
+ funcao_extracao: Função que retorna um DataFrame.
93
+ data_extracao: Data que será usada no nome do arquivo (ex: "2025_01_19").
94
+ diretorio_base: Diretório onde o arquivo será salvo.
95
+ falhar_se_vazio: Se True, encerra com sys.exit(1) quando DataFrame for vazio.
96
+ Se False, apenas loga warning e retorna None. Defaults to False.
97
+
98
+ Returns:
99
+ str: Caminho do arquivo salvo, ou None se DataFrame estiver vazio e falhar_se_vazio=False.
100
+
101
+ Raises:
102
+ SystemExit: Se houver erro na extração, ao salvar o arquivo,
103
+ ou se DataFrame for vazio e falhar_se_vazio=True.
104
+
105
+ Examples:
106
+ >>> from nia_etl_utils.processa_csv import extrair_e_exportar_csv
107
+ >>>
108
+ >>> def extrair_dados():
109
+ ... return pd.DataFrame({"col1": [1, 2, 3]})
110
+ >>>
111
+ >>> caminho = extrair_e_exportar_csv(
112
+ ... nome_extracao="dados_clientes",
113
+ ... funcao_extracao=extrair_dados,
114
+ ... data_extracao="2025_01_19",
115
+ ... diretorio_base="/tmp/dados"
116
+ ... )
117
+ """
118
+ try:
119
+ logger.info(f"Iniciando extração: {nome_extracao}")
120
+
121
+ # Executa função de extração
122
+ df_extraido = funcao_extracao()
123
+
124
+ # Valida resultado
125
+ if df_extraido is None or df_extraido.empty:
126
+ mensagem = f"Nenhum dado retornado para extração '{nome_extracao}'"
127
+
128
+ if falhar_se_vazio:
129
+ logger.error(mensagem)
130
+ sys.exit(1)
131
+ else:
132
+ logger.warning(mensagem)
133
+ return None
134
+
135
+ # Exporta para CSV
136
+ caminho = exportar_para_csv(
137
+ df=df_extraido,
138
+ nome_arquivo=nome_extracao,
139
+ data_extracao=data_extracao,
140
+ diretorio_base=diretorio_base,
141
+ )
142
+
143
+ logger.success(f"Extração concluída com sucesso: {nome_extracao}")
144
+ return caminho
145
+
146
+ except Exception as error:
147
+ logger.error(f"Erro ao extrair ou salvar '{nome_extracao}': {error}")
148
+ sys.exit(1)
149
+
150
+
151
+ def exportar_multiplos_csv(
152
+ extractions: list[dict],
153
+ data_extracao: str,
154
+ diretorio_base: str,
155
+ falhar_se_vazio: bool = False
156
+ ) -> dict[str, Optional[str]]:
157
+ """Executa múltiplas extrações e salva cada uma como CSV.
158
+
159
+ Args:
160
+ extractions: Lista de dicionários com 'nome' e 'funcao' para cada extração.
161
+ data_extracao: Data que será usada nos nomes dos arquivos.
162
+ diretorio_base: Diretório onde os arquivos serão salvos.
163
+ falhar_se_vazio: Se True, encerra quando algum DataFrame for vazio.
164
+
165
+ Returns:
166
+ dict: Mapeamento {nome_extracao: caminho_arquivo} para cada extração bem-sucedida.
167
+
168
+ Examples:
169
+ >>> from nia_etl_utils.processa_csv import exportar_multiplos_csv
170
+ >>>
171
+ >>> def extrair_clientes():
172
+ ... return pd.DataFrame({"id": [1, 2]})
173
+ >>>
174
+ >>> def extrair_vendas():
175
+ ... return pd.DataFrame({"valor": [100, 200]})
176
+ >>>
177
+ >>> extractions = [
178
+ ... {"nome": "clientes", "funcao": extrair_clientes},
179
+ ... {"nome": "vendas", "funcao": extrair_vendas}
180
+ ... ]
181
+ >>>
182
+ >>> resultados = exportar_multiplos_csv(
183
+ ... extractions=extractions,
184
+ ... data_extracao="2025_01_19",
185
+ ... diretorio_base="/tmp/dados"
186
+ ... )
187
+ """
188
+ resultados = {}
189
+
190
+ logger.info(f"Iniciando {len(extractions)} extrações em lote")
191
+
192
+ for extracao in extractions:
193
+ nome = extracao["nome"]
194
+ funcao = extracao["funcao"]
195
+
196
+ caminho = extrair_e_exportar_csv(
197
+ nome_extracao=nome,
198
+ funcao_extracao=funcao,
199
+ data_extracao=data_extracao,
200
+ diretorio_base=diretorio_base,
201
+ falhar_se_vazio=falhar_se_vazio
202
+ )
203
+
204
+ resultados[nome] = caminho
205
+
206
+ sucesso = sum(1 for v in resultados.values() if v is not None)
207
+ logger.info(f"Extrações concluídas: {sucesso}/{len(extractions)} bem-sucedidas")
208
+
209
+ return resultados
@@ -0,0 +1,151 @@
1
+ """Processamento paralelo de arquivos CSV grandes."""
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Callable, List, Optional
5
+ from multiprocessing import Pool, cpu_count
6
+
7
+ import pandas as pd
8
+ from loguru import logger
9
+
10
+
11
+ def calcular_chunksize(caminho_arquivo: str) -> int:
12
+ """Calcula tamanho ideal de chunk baseado no tamanho do arquivo.
13
+
14
+ Args:
15
+ caminho_arquivo: Caminho do arquivo CSV.
16
+
17
+ Returns:
18
+ int: Tamanho do chunk otimizado.
19
+
20
+ Examples:
21
+ >>> chunksize = calcular_chunksize("dados_grandes.csv")
22
+ >>> # Arquivo < 500MB: 10000 linhas
23
+ >>> # Arquivo 500MB-2GB: 5000 linhas
24
+ >>> # Arquivo 2-5GB: 2000 linhas
25
+ >>> # Arquivo > 5GB: 1000 linhas
26
+ """
27
+ tamanho_mb = Path(caminho_arquivo).stat().st_size / (1024 * 1024)
28
+
29
+ if tamanho_mb < 500:
30
+ return 10000
31
+ elif tamanho_mb < 2000:
32
+ return 5000
33
+ elif tamanho_mb < 5000:
34
+ return 2000
35
+ else:
36
+ return 1000
37
+
38
+
39
+ def _processar_chunk(args: tuple) -> pd.DataFrame:
40
+ """Processa um chunk aplicando transformações.
41
+
42
+ Função interna usada pelo Pool.imap().
43
+ """
44
+ chunk, colunas_para_tratar, func_tratar_texto, normalizar_colunas = args
45
+
46
+ # Aplica transformação nas colunas especificadas
47
+ for coluna in colunas_para_tratar:
48
+ if coluna in chunk.columns:
49
+ chunk[coluna] = chunk[coluna].apply(func_tratar_texto)
50
+ else:
51
+ logger.warning(f"Coluna '{coluna}' não encontrada no chunk")
52
+
53
+ # Normaliza nomes de colunas se solicitado
54
+ if normalizar_colunas:
55
+ chunk.columns = [col.lower() for col in chunk.columns]
56
+
57
+ return chunk
58
+
59
+
60
+ def processar_csv_paralelo(
61
+ caminho_entrada: str,
62
+ caminho_saida: str,
63
+ colunas_para_tratar: List[str],
64
+ funcao_transformacao: Callable,
65
+ chunksize: Optional[int] = None,
66
+ normalizar_colunas: bool = True,
67
+ remover_entrada: bool = False,
68
+ num_processos: Optional[int] = None
69
+ ) -> None:
70
+ """Processa CSV grande em paralelo aplicando transformações por chunk.
71
+
72
+ Args:
73
+ caminho_entrada: Arquivo CSV de entrada.
74
+ caminho_saida: Arquivo CSV de saída.
75
+ colunas_para_tratar: Lista de colunas para aplicar transformação.
76
+ funcao_transformacao: Função que recebe valor e retorna valor transformado.
77
+ chunksize: Tamanho do chunk. Se None, calcula automaticamente.
78
+ normalizar_colunas: Se True, converte nomes de colunas para lowercase.
79
+ remover_entrada: Se True, remove arquivo de entrada após processar.
80
+ num_processos: Número de processos paralelos. Se None, usa cpu_count().
81
+
82
+ Raises:
83
+ SystemExit: Se arquivo de entrada não existe ou erro no processamento.
84
+
85
+ Examples:
86
+ >>> from nia_etl_utils import processar_csv_paralelo
87
+ >>>
88
+ >>> def limpar_texto(texto):
89
+ ... return texto.strip().upper()
90
+ >>>
91
+ >>> processar_csv_paralelo(
92
+ ... caminho_entrada="dados_brutos.csv",
93
+ ... caminho_saida="dados_limpos.csv",
94
+ ... colunas_para_tratar=["nome", "descricao"],
95
+ ... funcao_transformacao=limpar_texto,
96
+ ... remover_entrada=True
97
+ ... )
98
+ """
99
+ caminho_entrada_path = Path(caminho_entrada)
100
+
101
+ # Validação de entrada
102
+ if not caminho_entrada_path.exists():
103
+ logger.error(f"Arquivo de entrada não encontrado: {caminho_entrada}")
104
+ sys.exit(1)
105
+
106
+ try:
107
+ logger.info(f"Iniciando processamento paralelo: {caminho_entrada}")
108
+
109
+ # Define chunksize
110
+ if chunksize is None:
111
+ chunksize = calcular_chunksize(caminho_entrada)
112
+
113
+ logger.info(f"Chunksize: {chunksize} linhas | Processos: {num_processos or cpu_count()}")
114
+
115
+ # Processamento paralelo
116
+ primeiro_chunk = True
117
+
118
+ with Pool(processes=num_processos or cpu_count()) as pool:
119
+ reader = pd.read_csv(caminho_entrada, chunksize=chunksize)
120
+
121
+ # Prepara tasks para processamento paralelo
122
+ tasks = [
123
+ (chunk, colunas_para_tratar, funcao_transformacao, normalizar_colunas)
124
+ for chunk in reader
125
+ ]
126
+
127
+ # Processa chunks em paralelo
128
+ for i, chunk_processado in enumerate(pool.imap(_processar_chunk, tasks), start=1):
129
+ logger.info(f"Escrevendo chunk {i} ({len(chunk_processado)} linhas)")
130
+
131
+ chunk_processado.to_csv(
132
+ caminho_saida,
133
+ mode='w' if primeiro_chunk else 'a',
134
+ header=primeiro_chunk,
135
+ index=False
136
+ )
137
+ primeiro_chunk = False
138
+
139
+ logger.success(f"Processamento concluído: {caminho_saida}")
140
+
141
+ # Remove arquivo de entrada se solicitado
142
+ if remover_entrada:
143
+ try:
144
+ caminho_entrada_path.unlink()
145
+ logger.info(f"Arquivo de entrada removido: {caminho_entrada}")
146
+ except Exception as e:
147
+ logger.warning(f"Falha ao remover arquivo de entrada: {e}")
148
+
149
+ except Exception as error:
150
+ logger.exception(f"Erro no processamento paralelo: {error}")
151
+ sys.exit(1)