nia-etl-utils 0.1.0__py3-none-any.whl → 0.2.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 +154 -44
- nia_etl_utils/config.py +391 -0
- nia_etl_utils/database.py +249 -153
- nia_etl_utils/email_smtp.py +201 -67
- nia_etl_utils/env_config.py +137 -15
- nia_etl_utils/exceptions.py +327 -0
- nia_etl_utils/limpeza_pastas.py +192 -59
- nia_etl_utils/logger_config.py +98 -40
- nia_etl_utils/processa_csv.py +257 -114
- nia_etl_utils/processa_csv_paralelo.py +150 -37
- nia_etl_utils/results.py +304 -0
- {nia_etl_utils-0.1.0.dist-info → nia_etl_utils-0.2.0.dist-info}/METADATA +22 -1
- nia_etl_utils-0.2.0.dist-info/RECORD +15 -0
- nia_etl_utils-0.1.0.dist-info/RECORD +0 -12
- {nia_etl_utils-0.1.0.dist-info → nia_etl_utils-0.2.0.dist-info}/WHEEL +0 -0
- {nia_etl_utils-0.1.0.dist-info → nia_etl_utils-0.2.0.dist-info}/top_level.txt +0 -0
nia_etl_utils/logger_config.py
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
"""Módulo utilitário para configuração de logging com Loguru.
|
|
2
|
-
|
|
1
|
+
"""Módulo utilitário para configuração de logging com Loguru.
|
|
2
|
+
|
|
3
|
+
Fornece funções para configurar o logger com rotação automática,
|
|
4
|
+
retenção configurável e estrutura de diretórios organizada.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Configuração básica:
|
|
8
|
+
|
|
9
|
+
>>> from nia_etl_utils import configurar_logger
|
|
10
|
+
>>> caminho = configurar_logger("extract", "2025_01_20")
|
|
11
|
+
>>> logger.info("Pipeline iniciado")
|
|
12
|
+
|
|
13
|
+
Configuração com padrões NIA:
|
|
14
|
+
|
|
15
|
+
>>> from nia_etl_utils import configurar_logger_padrao_nia
|
|
16
|
+
>>> caminho = configurar_logger_padrao_nia("ouvidorias_etl")
|
|
17
|
+
|
|
18
|
+
Configuração customizada:
|
|
19
|
+
|
|
20
|
+
>>> caminho = configurar_logger(
|
|
21
|
+
... prefixo="etl_vendas",
|
|
22
|
+
... data_extracao="2025_01_20",
|
|
23
|
+
... pasta_logs="/var/log/nia",
|
|
24
|
+
... rotation="50 MB",
|
|
25
|
+
... retention="30 days",
|
|
26
|
+
... level="INFO"
|
|
27
|
+
... )
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from datetime import datetime
|
|
3
31
|
from pathlib import Path
|
|
32
|
+
|
|
4
33
|
from loguru import logger
|
|
5
34
|
|
|
35
|
+
from .exceptions import DiretorioError, ValidacaoError
|
|
36
|
+
|
|
6
37
|
|
|
7
38
|
def configurar_logger(
|
|
8
39
|
prefixo: str,
|
|
@@ -14,32 +45,43 @@ def configurar_logger(
|
|
|
14
45
|
) -> str:
|
|
15
46
|
"""Configura o logger da aplicação com Loguru.
|
|
16
47
|
|
|
17
|
-
Cria um handler de arquivo para o logger com rotação automática
|
|
18
|
-
configurável. O arquivo de log é criado em uma estrutura
|
|
19
|
-
organizada por prefixo.
|
|
48
|
+
Cria um handler de arquivo para o logger com rotação automática
|
|
49
|
+
e retenção configurável. O arquivo de log é criado em uma estrutura
|
|
50
|
+
de diretórios organizada por prefixo.
|
|
20
51
|
|
|
21
52
|
Args:
|
|
22
53
|
prefixo: Nome do módulo/pipeline (ex: 'extract', 'transform', 'load').
|
|
54
|
+
Usado para criar subdiretório e nomear arquivo.
|
|
23
55
|
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.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
pasta_logs: Diretório raiz onde os logs serão armazenados.
|
|
57
|
+
Defaults to "logs".
|
|
58
|
+
rotation: Critério de rotação do arquivo. Pode ser tamanho
|
|
59
|
+
("10 MB", "500 KB") ou tempo ("1 day", "1 week").
|
|
60
|
+
Defaults to "10 MB".
|
|
61
|
+
retention: Tempo de retenção dos logs antigos antes de serem
|
|
62
|
+
removidos. Defaults to "7 days".
|
|
63
|
+
level: Nível mínimo de log a ser registrado. Opções: DEBUG,
|
|
64
|
+
INFO, WARNING, ERROR, CRITICAL. Defaults to "DEBUG".
|
|
28
65
|
|
|
29
66
|
Returns:
|
|
30
|
-
|
|
67
|
+
Caminho completo do arquivo de log criado.
|
|
31
68
|
|
|
32
69
|
Raises:
|
|
33
|
-
|
|
70
|
+
ValidacaoError: Se prefixo ou data_extracao forem vazios.
|
|
71
|
+
DiretorioError: Se houver erro ao criar diretórios de log.
|
|
34
72
|
|
|
35
73
|
Examples:
|
|
36
|
-
|
|
37
|
-
|
|
74
|
+
Configuração básica:
|
|
75
|
+
|
|
76
|
+
>>> from nia_etl_utils import configurar_logger
|
|
77
|
+
>>> from loguru import logger
|
|
78
|
+
>>> caminho = configurar_logger("extract", "2025_01_19")
|
|
38
79
|
>>> logger.info("Pipeline iniciado")
|
|
39
80
|
>>> # Log salvo em: logs/extract/extract_2025_01_19.log
|
|
40
81
|
|
|
41
|
-
|
|
42
|
-
|
|
82
|
+
Com configurações customizadas:
|
|
83
|
+
|
|
84
|
+
>>> caminho = configurar_logger(
|
|
43
85
|
... prefixo="etl_ouvidorias",
|
|
44
86
|
... data_extracao="2025_01_19",
|
|
45
87
|
... pasta_logs="/var/logs/nia",
|
|
@@ -48,16 +90,20 @@ def configurar_logger(
|
|
|
48
90
|
... level="INFO"
|
|
49
91
|
... )
|
|
50
92
|
"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
93
|
+
# Validações
|
|
94
|
+
if not prefixo or not prefixo.strip():
|
|
95
|
+
raise ValidacaoError(
|
|
96
|
+
"Prefixo não pode ser vazio",
|
|
97
|
+
details={"parametro": "prefixo", "valor": prefixo}
|
|
98
|
+
)
|
|
56
99
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
100
|
+
if not data_extracao or not data_extracao.strip():
|
|
101
|
+
raise ValidacaoError(
|
|
102
|
+
"Data de extração não pode ser vazia",
|
|
103
|
+
details={"parametro": "data_extracao", "valor": data_extracao}
|
|
104
|
+
)
|
|
60
105
|
|
|
106
|
+
try:
|
|
61
107
|
# Cria estrutura de diretórios
|
|
62
108
|
diretorio_log = Path(pasta_logs) / prefixo
|
|
63
109
|
diretorio_log.mkdir(parents=True, exist_ok=True)
|
|
@@ -79,15 +125,16 @@ def configurar_logger(
|
|
|
79
125
|
|
|
80
126
|
return str(caminho_log)
|
|
81
127
|
|
|
82
|
-
except PermissionError as
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
except PermissionError as e:
|
|
129
|
+
raise DiretorioError(
|
|
130
|
+
f"Sem permissão para criar diretório de logs '{pasta_logs}'",
|
|
131
|
+
details={"pasta": pasta_logs, "erro": str(e)}
|
|
132
|
+
) from e
|
|
133
|
+
except OSError as e:
|
|
134
|
+
raise DiretorioError(
|
|
135
|
+
f"Erro do sistema ao configurar logger em '{pasta_logs}'",
|
|
136
|
+
details={"pasta": pasta_logs, "erro": str(e)}
|
|
137
|
+
) from e
|
|
91
138
|
|
|
92
139
|
|
|
93
140
|
def configurar_logger_padrao_nia(nome_pipeline: str) -> str:
|
|
@@ -104,17 +151,19 @@ def configurar_logger_padrao_nia(nome_pipeline: str) -> str:
|
|
|
104
151
|
nome_pipeline: Nome do pipeline (será usado como prefixo e na data).
|
|
105
152
|
|
|
106
153
|
Returns:
|
|
107
|
-
|
|
154
|
+
Caminho completo do arquivo de log criado.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValidacaoError: Se nome_pipeline for vazio.
|
|
158
|
+
DiretorioError: Se houver erro ao criar diretórios.
|
|
108
159
|
|
|
109
160
|
Examples:
|
|
110
|
-
>>> from nia_etl_utils
|
|
111
|
-
>>> from
|
|
161
|
+
>>> from nia_etl_utils import configurar_logger_padrao_nia
|
|
162
|
+
>>> from loguru import logger
|
|
112
163
|
>>>
|
|
113
|
-
>>>
|
|
164
|
+
>>> caminho = configurar_logger_padrao_nia("ouvidorias_etl")
|
|
114
165
|
>>> logger.info("Pipeline iniciado com configurações padrão NIA")
|
|
115
166
|
"""
|
|
116
|
-
from datetime import datetime
|
|
117
|
-
|
|
118
167
|
data_hoje = datetime.now().strftime("%Y_%m_%d")
|
|
119
168
|
|
|
120
169
|
return configurar_logger(
|
|
@@ -133,8 +182,12 @@ def remover_handlers_existentes() -> None:
|
|
|
133
182
|
Útil quando você precisa reconfigurar o logger do zero ou quando está
|
|
134
183
|
rodando múltiplos scripts em sequência que configuram o logger.
|
|
135
184
|
|
|
185
|
+
Note:
|
|
186
|
+
Após chamar esta função, apenas o handler padrão (stderr) estará
|
|
187
|
+
ativo. Chame configurar_logger() para adicionar novos handlers.
|
|
188
|
+
|
|
136
189
|
Examples:
|
|
137
|
-
>>> from nia_etl_utils
|
|
190
|
+
>>> from nia_etl_utils import remover_handlers_existentes, configurar_logger
|
|
138
191
|
>>>
|
|
139
192
|
>>> # Remove handlers anteriores
|
|
140
193
|
>>> remover_handlers_existentes()
|
|
@@ -143,4 +196,9 @@ def remover_handlers_existentes() -> None:
|
|
|
143
196
|
>>> configurar_logger("novo_pipeline", "2025_01_19")
|
|
144
197
|
"""
|
|
145
198
|
logger.remove()
|
|
146
|
-
logger.
|
|
199
|
+
logger.add(
|
|
200
|
+
sink=lambda msg: print(msg, end=""),
|
|
201
|
+
level="DEBUG",
|
|
202
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}"
|
|
203
|
+
)
|
|
204
|
+
logger.debug("Handlers do logger foram resetados.")
|
nia_etl_utils/processa_csv.py
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
"""Módulo para exportação de DataFrame para CSV.
|
|
2
2
|
|
|
3
3
|
Fornece funções utilitárias para salvar DataFrames em formato CSV com
|
|
4
|
-
nomenclatura padronizada e
|
|
4
|
+
nomenclatura padronizada, logging adequado e tratamento de erros.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Exportação simples:
|
|
8
|
+
|
|
9
|
+
>>> from nia_etl_utils import exportar_para_csv
|
|
10
|
+
>>> import pandas as pd
|
|
11
|
+
>>> df = pd.DataFrame({"id": [1, 2], "valor": [100, 200]})
|
|
12
|
+
>>> caminho = exportar_para_csv(df, "vendas", "2025_01_20", "/tmp/dados")
|
|
13
|
+
|
|
14
|
+
Extração e exportação:
|
|
15
|
+
|
|
16
|
+
>>> from nia_etl_utils import extrair_e_exportar_csv
|
|
17
|
+
>>> def extrair_clientes():
|
|
18
|
+
... return pd.DataFrame({"id": [1, 2], "nome": ["Ana", "João"]})
|
|
19
|
+
>>> resultado = extrair_e_exportar_csv(
|
|
20
|
+
... nome_extracao="clientes",
|
|
21
|
+
... funcao_extracao=extrair_clientes,
|
|
22
|
+
... data_extracao="2025_01_20",
|
|
23
|
+
... diretorio_base="/tmp/dados"
|
|
24
|
+
... )
|
|
25
|
+
|
|
26
|
+
Múltiplas extrações:
|
|
27
|
+
|
|
28
|
+
>>> extractions = [
|
|
29
|
+
... {"nome": "clientes", "funcao": extrair_clientes},
|
|
30
|
+
... {"nome": "vendas", "funcao": extrair_vendas},
|
|
31
|
+
... ]
|
|
32
|
+
>>> resultados = exportar_multiplos_csv(extractions, "2025_01_20", "/tmp")
|
|
5
33
|
"""
|
|
6
|
-
|
|
34
|
+
|
|
35
|
+
from collections.abc import Callable
|
|
7
36
|
from pathlib import Path
|
|
8
|
-
|
|
37
|
+
|
|
9
38
|
import pandas as pd
|
|
10
39
|
from loguru import logger
|
|
11
40
|
|
|
41
|
+
from .exceptions import (
|
|
42
|
+
EscritaArquivoError,
|
|
43
|
+
ExtracaoError,
|
|
44
|
+
ExtracaoVaziaError,
|
|
45
|
+
ValidacaoError,
|
|
46
|
+
)
|
|
47
|
+
from .results import ResultadoExtracao, ResultadoLote
|
|
48
|
+
|
|
12
49
|
|
|
13
50
|
def exportar_para_csv(
|
|
14
51
|
df: pd.DataFrame,
|
|
@@ -18,36 +55,59 @@ def exportar_para_csv(
|
|
|
18
55
|
) -> str:
|
|
19
56
|
"""Salva um DataFrame como arquivo CSV.
|
|
20
57
|
|
|
58
|
+
Cria o diretório de destino se não existir e salva o DataFrame
|
|
59
|
+
com nomenclatura padronizada: {nome_arquivo}_{data_extracao}.csv
|
|
60
|
+
|
|
21
61
|
Args:
|
|
22
|
-
df: DataFrame a ser salvo.
|
|
23
|
-
nome_arquivo: Nome base do arquivo (sem extensão).
|
|
24
|
-
data_extracao: Data
|
|
25
|
-
|
|
62
|
+
df: DataFrame a ser salvo. Não pode ser None ou vazio.
|
|
63
|
+
nome_arquivo: Nome base do arquivo (sem extensão e sem data).
|
|
64
|
+
data_extracao: Data no formato string para compor o nome
|
|
65
|
+
do arquivo (ex: "2025_01_19").
|
|
66
|
+
diretorio_base: Caminho do diretório onde o arquivo será salvo.
|
|
67
|
+
Será criado se não existir.
|
|
26
68
|
|
|
27
69
|
Returns:
|
|
28
|
-
|
|
70
|
+
Caminho absoluto do arquivo CSV salvo.
|
|
29
71
|
|
|
30
72
|
Raises:
|
|
31
|
-
|
|
73
|
+
ExtracaoVaziaError: Se df for None ou vazio.
|
|
74
|
+
ValidacaoError: Se nome_arquivo for vazio ou apenas espaços.
|
|
75
|
+
EscritaArquivoError: Se houver erro de permissão ou sistema
|
|
76
|
+
ao salvar o arquivo.
|
|
32
77
|
|
|
33
78
|
Examples:
|
|
79
|
+
Exportação básica:
|
|
80
|
+
|
|
34
81
|
>>> import pandas as pd
|
|
35
|
-
>>>
|
|
36
|
-
>>>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
82
|
+
>>> df = pd.DataFrame({"id": [1, 2], "valor": [100, 200]})
|
|
83
|
+
>>> caminho = exportar_para_csv(
|
|
84
|
+
... df=df,
|
|
85
|
+
... nome_arquivo="vendas",
|
|
86
|
+
... data_extracao="2025_01_19",
|
|
87
|
+
... diretorio_base="/tmp/dados"
|
|
88
|
+
... )
|
|
89
|
+
>>> print(caminho)
|
|
90
|
+
/tmp/dados/vendas_2025_01_19.csv
|
|
91
|
+
|
|
92
|
+
Com tratamento de erro:
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
94
|
+
>>> from nia_etl_utils.exceptions import ExtracaoVaziaError
|
|
95
|
+
>>> try:
|
|
96
|
+
... exportar_para_csv(pd.DataFrame(), "vazio", "2025_01_19", "/tmp")
|
|
97
|
+
... except ExtracaoVaziaError:
|
|
98
|
+
... print("DataFrame estava vazio")
|
|
99
|
+
"""
|
|
100
|
+
# Validações
|
|
101
|
+
if df is None or df.empty:
|
|
102
|
+
raise ExtracaoVaziaError(nome_arquivo)
|
|
103
|
+
|
|
104
|
+
if not nome_arquivo or not nome_arquivo.strip():
|
|
105
|
+
raise ValidacaoError(
|
|
106
|
+
"Nome do arquivo não pode ser vazio",
|
|
107
|
+
details={"parametro": "nome_arquivo", "valor": nome_arquivo}
|
|
108
|
+
)
|
|
50
109
|
|
|
110
|
+
try:
|
|
51
111
|
# Cria diretório se não existir
|
|
52
112
|
diretorio = Path(diretorio_base)
|
|
53
113
|
diretorio.mkdir(parents=True, exist_ok=True)
|
|
@@ -67,15 +127,16 @@ def exportar_para_csv(
|
|
|
67
127
|
|
|
68
128
|
return str(caminho_arquivo)
|
|
69
129
|
|
|
70
|
-
except PermissionError as
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
except PermissionError as e:
|
|
131
|
+
raise EscritaArquivoError(
|
|
132
|
+
f"Sem permissão para salvar arquivo em '{diretorio_base}'",
|
|
133
|
+
details={"diretorio": diretorio_base, "erro": str(e)}
|
|
134
|
+
) from e
|
|
135
|
+
except OSError as e:
|
|
136
|
+
raise EscritaArquivoError(
|
|
137
|
+
f"Erro do sistema ao salvar CSV em '{diretorio_base}'",
|
|
138
|
+
details={"diretorio": diretorio_base, "erro": str(e)}
|
|
139
|
+
) from e
|
|
79
140
|
|
|
80
141
|
|
|
81
142
|
def extrair_e_exportar_csv(
|
|
@@ -83,127 +144,209 @@ def extrair_e_exportar_csv(
|
|
|
83
144
|
funcao_extracao: Callable[[], pd.DataFrame],
|
|
84
145
|
data_extracao: str,
|
|
85
146
|
diretorio_base: str,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
147
|
+
) -> ResultadoExtracao:
|
|
148
|
+
"""Executa função de extração e salva resultado como CSV.
|
|
149
|
+
|
|
150
|
+
Orquestra o fluxo completo: executa a função fornecida, valida
|
|
151
|
+
o DataFrame retornado e persiste como CSV no diretório especificado.
|
|
89
152
|
|
|
90
153
|
Args:
|
|
91
|
-
nome_extracao:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
154
|
+
nome_extracao: Identificador da extração. Usado no nome do arquivo
|
|
155
|
+
e nos logs.
|
|
156
|
+
funcao_extracao: Callable sem argumentos que retorna pd.DataFrame.
|
|
157
|
+
Será executada dentro de try/except para captura de erros.
|
|
158
|
+
data_extracao: Data no formato string para compor o nome do arquivo
|
|
159
|
+
(ex: "2025_01_19").
|
|
160
|
+
diretorio_base: Caminho do diretório onde o arquivo será salvo.
|
|
97
161
|
|
|
98
162
|
Returns:
|
|
99
|
-
|
|
163
|
+
ResultadoExtracao contendo:
|
|
164
|
+
- nome: Identificador da extração
|
|
165
|
+
- caminho: Path do arquivo salvo
|
|
166
|
+
- linhas: Quantidade de registros extraídos
|
|
167
|
+
- sucesso: True se exportação completou
|
|
168
|
+
- erro: None se sucesso, mensagem se falha
|
|
169
|
+
- colunas: Quantidade de colunas
|
|
170
|
+
- tamanho_bytes: Tamanho do arquivo
|
|
100
171
|
|
|
101
172
|
Raises:
|
|
102
|
-
|
|
103
|
-
|
|
173
|
+
ExtracaoVaziaError: Se a função retornar DataFrame vazio ou None.
|
|
174
|
+
ExtracaoError: Se houver erro na execução da função de extração.
|
|
175
|
+
ValidacaoError: Se parâmetros de exportação forem inválidos.
|
|
176
|
+
EscritaArquivoError: Se houver erro ao persistir o arquivo.
|
|
104
177
|
|
|
105
178
|
Examples:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
>>> def
|
|
109
|
-
... return pd.DataFrame({"
|
|
110
|
-
|
|
111
|
-
>>>
|
|
112
|
-
... nome_extracao="
|
|
113
|
-
... funcao_extracao=
|
|
179
|
+
Extração simples:
|
|
180
|
+
|
|
181
|
+
>>> def extrair_clientes():
|
|
182
|
+
... return pd.DataFrame({"id": [1, 2], "nome": ["Ana", "João"]})
|
|
183
|
+
...
|
|
184
|
+
>>> resultado = extrair_e_exportar_csv(
|
|
185
|
+
... nome_extracao="clientes",
|
|
186
|
+
... funcao_extracao=extrair_clientes,
|
|
114
187
|
... data_extracao="2025_01_19",
|
|
115
188
|
... diretorio_base="/tmp/dados"
|
|
116
189
|
... )
|
|
190
|
+
>>> resultado.sucesso
|
|
191
|
+
True
|
|
192
|
+
>>> resultado.linhas
|
|
193
|
+
2
|
|
194
|
+
|
|
195
|
+
Tratando extração vazia:
|
|
196
|
+
|
|
197
|
+
>>> from nia_etl_utils.exceptions import ExtracaoVaziaError
|
|
198
|
+
>>> def extrair_vazia():
|
|
199
|
+
... return pd.DataFrame()
|
|
200
|
+
...
|
|
201
|
+
>>> try:
|
|
202
|
+
... extrair_e_exportar_csv(
|
|
203
|
+
... nome_extracao="vazia",
|
|
204
|
+
... funcao_extracao=extrair_vazia,
|
|
205
|
+
... data_extracao="2025_01_19",
|
|
206
|
+
... diretorio_base="/tmp/dados"
|
|
207
|
+
... )
|
|
208
|
+
... except ExtracaoVaziaError as e:
|
|
209
|
+
... print(f"Esperado: {e.nome_extracao}")
|
|
210
|
+
Esperado: vazia
|
|
117
211
|
"""
|
|
118
|
-
|
|
119
|
-
logger.info(f"Iniciando extração: {nome_extracao}")
|
|
212
|
+
logger.info(f"Iniciando extração: {nome_extracao}")
|
|
120
213
|
|
|
121
|
-
|
|
214
|
+
# Executa função de extração
|
|
215
|
+
try:
|
|
122
216
|
df_extraido = funcao_extracao()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
217
|
+
except Exception as e:
|
|
218
|
+
raise ExtracaoError(
|
|
219
|
+
f"Erro ao executar extração '{nome_extracao}'",
|
|
220
|
+
details={"extracao": nome_extracao, "erro": str(e)}
|
|
221
|
+
) from e
|
|
222
|
+
|
|
223
|
+
# Valida resultado
|
|
224
|
+
if df_extraido is None or df_extraido.empty:
|
|
225
|
+
raise ExtracaoVaziaError(nome_extracao)
|
|
226
|
+
|
|
227
|
+
# Exporta para CSV
|
|
228
|
+
caminho = exportar_para_csv(
|
|
229
|
+
df=df_extraido,
|
|
230
|
+
nome_arquivo=nome_extracao,
|
|
231
|
+
data_extracao=data_extracao,
|
|
232
|
+
diretorio_base=diretorio_base,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Coleta métricas
|
|
236
|
+
tamanho_bytes = Path(caminho).stat().st_size
|
|
237
|
+
|
|
238
|
+
logger.success(f"Extração concluída: {nome_extracao}")
|
|
239
|
+
|
|
240
|
+
return ResultadoExtracao(
|
|
241
|
+
nome=nome_extracao,
|
|
242
|
+
caminho=caminho,
|
|
243
|
+
linhas=len(df_extraido),
|
|
244
|
+
sucesso=True,
|
|
245
|
+
colunas=len(df_extraido.columns),
|
|
246
|
+
tamanho_bytes=tamanho_bytes
|
|
247
|
+
)
|
|
149
248
|
|
|
150
249
|
|
|
151
250
|
def exportar_multiplos_csv(
|
|
152
251
|
extractions: list[dict],
|
|
153
252
|
data_extracao: str,
|
|
154
253
|
diretorio_base: str,
|
|
155
|
-
|
|
156
|
-
) ->
|
|
157
|
-
"""Executa múltiplas extrações e salva cada uma como CSV.
|
|
254
|
+
ignorar_vazios: bool = True
|
|
255
|
+
) -> ResultadoLote:
|
|
256
|
+
"""Executa múltiplas extrações em lote e salva cada uma como CSV.
|
|
257
|
+
|
|
258
|
+
Itera sobre a lista de extrações, executando cada uma sequencialmente.
|
|
259
|
+
O comportamento ao encontrar extrações vazias é controlado pelo
|
|
260
|
+
parâmetro ignorar_vazios.
|
|
158
261
|
|
|
159
262
|
Args:
|
|
160
|
-
extractions: Lista de dicionários
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
263
|
+
extractions: Lista de dicionários, cada um contendo:
|
|
264
|
+
- nome (str): Identificador da extração
|
|
265
|
+
- funcao (Callable[[], pd.DataFrame]): Função de extração
|
|
266
|
+
data_extracao: Data no formato string para compor os nomes dos
|
|
267
|
+
arquivos (ex: "2025_01_19").
|
|
268
|
+
diretorio_base: Caminho do diretório onde os arquivos serão salvos.
|
|
269
|
+
ignorar_vazios: Comportamento quando uma extração retorna vazio.
|
|
270
|
+
Se True (default), loga warning e continua com as próximas.
|
|
271
|
+
Se False, levanta ExtracaoVaziaError imediatamente.
|
|
164
272
|
|
|
165
273
|
Returns:
|
|
166
|
-
|
|
274
|
+
ResultadoLote contendo lista de ResultadoExtracao e métricas
|
|
275
|
+
consolidadas (total, sucesso, falhas, taxa de sucesso).
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ExtracaoVaziaError: Se ignorar_vazios=False e alguma extração
|
|
279
|
+
retornar DataFrame vazio ou None.
|
|
280
|
+
ExtracaoError: Se houver erro crítico em alguma extração
|
|
281
|
+
(não relacionado a dados vazios).
|
|
167
282
|
|
|
168
283
|
Examples:
|
|
169
|
-
|
|
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
|
-
>>>
|
|
284
|
+
Múltiplas extrações tolerando vazios:
|
|
285
|
+
|
|
177
286
|
>>> extractions = [
|
|
178
287
|
... {"nome": "clientes", "funcao": extrair_clientes},
|
|
179
|
-
... {"nome": "vendas", "funcao": extrair_vendas}
|
|
288
|
+
... {"nome": "vendas", "funcao": extrair_vendas},
|
|
180
289
|
... ]
|
|
181
|
-
>>>
|
|
182
|
-
>>> resultados = exportar_multiplos_csv(
|
|
290
|
+
>>> lote = exportar_multiplos_csv(
|
|
183
291
|
... extractions=extractions,
|
|
184
292
|
... data_extracao="2025_01_19",
|
|
185
293
|
... diretorio_base="/tmp/dados"
|
|
186
294
|
... )
|
|
295
|
+
>>> print(f"{lote.sucesso}/{lote.total} bem-sucedidas")
|
|
296
|
+
>>> for r in lote.extracoes_sucesso:
|
|
297
|
+
... print(f"{r.nome}: {r.linhas} linhas")
|
|
298
|
+
|
|
299
|
+
Falhando na primeira extração vazia:
|
|
300
|
+
|
|
301
|
+
>>> try:
|
|
302
|
+
... exportar_multiplos_csv(
|
|
303
|
+
... extractions=extractions,
|
|
304
|
+
... data_extracao="2025_01_19",
|
|
305
|
+
... diretorio_base="/tmp/dados",
|
|
306
|
+
... ignorar_vazios=False
|
|
307
|
+
... )
|
|
308
|
+
... except ExtracaoVaziaError:
|
|
309
|
+
... print("Pipeline interrompido por extração vazia")
|
|
310
|
+
|
|
311
|
+
Verificando falhas:
|
|
312
|
+
|
|
313
|
+
>>> lote = exportar_multiplos_csv(extractions, "2025_01_19", "/tmp")
|
|
314
|
+
>>> if not lote.todos_sucesso:
|
|
315
|
+
... for falha in lote.extracoes_falhas:
|
|
316
|
+
... logger.warning(f"{falha.nome}: {falha.erro}")
|
|
187
317
|
"""
|
|
188
|
-
|
|
189
|
-
|
|
318
|
+
lote = ResultadoLote()
|
|
190
319
|
logger.info(f"Iniciando {len(extractions)} extrações em lote")
|
|
191
320
|
|
|
192
321
|
for extracao in extractions:
|
|
193
322
|
nome = extracao["nome"]
|
|
194
323
|
funcao = extracao["funcao"]
|
|
195
324
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
325
|
+
try:
|
|
326
|
+
resultado = extrair_e_exportar_csv(
|
|
327
|
+
nome_extracao=nome,
|
|
328
|
+
funcao_extracao=funcao,
|
|
329
|
+
data_extracao=data_extracao,
|
|
330
|
+
diretorio_base=diretorio_base,
|
|
331
|
+
)
|
|
332
|
+
lote.adicionar(resultado)
|
|
333
|
+
|
|
334
|
+
except ExtracaoVaziaError as e:
|
|
335
|
+
if ignorar_vazios:
|
|
336
|
+
logger.warning(str(e))
|
|
337
|
+
lote.adicionar(ResultadoExtracao(
|
|
338
|
+
nome=nome,
|
|
339
|
+
caminho=None,
|
|
340
|
+
linhas=0,
|
|
341
|
+
sucesso=False,
|
|
342
|
+
erro=str(e)
|
|
343
|
+
))
|
|
344
|
+
else:
|
|
345
|
+
raise
|
|
205
346
|
|
|
206
|
-
|
|
207
|
-
|
|
347
|
+
logger.info(
|
|
348
|
+
f"Extrações concluídas: {lote.sucesso}/{lote.total} bem-sucedidas "
|
|
349
|
+
f"({lote.taxa_sucesso:.0%})"
|
|
350
|
+
)
|
|
208
351
|
|
|
209
|
-
return
|
|
352
|
+
return lote
|