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.
- nia_etl_utils/__init__.py +126 -0
- nia_etl_utils/database.py +246 -0
- nia_etl_utils/email_smtp.py +126 -0
- nia_etl_utils/env_config.py +43 -0
- nia_etl_utils/limpeza_pastas.py +137 -0
- nia_etl_utils/logger_config.py +146 -0
- nia_etl_utils/processa_csv.py +209 -0
- nia_etl_utils/processa_csv_paralelo.py +151 -0
- nia_etl_utils-0.1.0.dist-info/METADATA +594 -0
- nia_etl_utils-0.1.0.dist-info/RECORD +12 -0
- nia_etl_utils-0.1.0.dist-info/WHEEL +5 -0
- nia_etl_utils-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|