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.
@@ -1,8 +1,39 @@
1
- """Módulo utilitário para configuração de logging com Loguru."""
2
- import sys
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 e retenção
18
- configurável. O arquivo de log é criado em uma estrutura de diretórios
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. 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".
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
- str: Caminho completo do arquivo de log criado.
67
+ Caminho completo do arquivo de log criado.
31
68
 
32
69
  Raises:
33
- SystemExit: Se houver erro ao criar diretórios ou configurar o logger.
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
- >>> from nia_etl_utils.logger_config import configurar_logger
37
- >>> caminho_log = configurar_logger("extract", "2025_01_19")
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
- >>> # Com configurações customizadas
42
- >>> caminho_log = configurar_logger(
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
- 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)
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
- 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)
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 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)
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
- str: Caminho completo do arquivo de log criado.
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.logger_config import configurar_logger_padrao_nia
111
- >>> from datetime import datetime
161
+ >>> from nia_etl_utils import configurar_logger_padrao_nia
162
+ >>> from loguru import logger
112
163
  >>>
113
- >>> caminho_log = configurar_logger_padrao_nia("ouvidorias_etl")
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.logger_config import remover_handlers_existentes, configurar_logger
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.info("Todos os handlers do logger foram removidos.")
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.")
@@ -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 logging adequado.
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
- import sys
34
+
35
+ from collections.abc import Callable
7
36
  from pathlib import Path
8
- from typing import Callable, Optional
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 que será usada no nome do arquivo (ex: "2025_01_19").
25
- diretorio_base: Diretório onde o arquivo será salvo.
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
- str: Caminho completo do arquivo salvo.
70
+ Caminho absoluto do arquivo CSV salvo.
29
71
 
30
72
  Raises:
31
- SystemExit: Se houver erro ao salvar o arquivo.
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
- >>> 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 ""
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
- if not nome_arquivo or not nome_arquivo.strip():
48
- logger.error("Nome do arquivo não pode ser vazio.")
49
- sys.exit(1)
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 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)
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
- falhar_se_vazio: bool = False
87
- ) -> Optional[str]:
88
- """Executa uma função de extração e salva o resultado como CSV.
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: 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.
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
- str: Caminho do arquivo salvo, ou None se DataFrame estiver vazio e falhar_se_vazio=False.
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
- SystemExit: Se houver erro na extração, ao salvar o arquivo,
103
- ou se DataFrame for vazio e falhar_se_vazio=True.
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
- >>> 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,
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
- try:
119
- logger.info(f"Iniciando extração: {nome_extracao}")
212
+ logger.info(f"Iniciando extração: {nome_extracao}")
120
213
 
121
- # Executa função de extração
214
+ # Executa função de extração
215
+ try:
122
216
  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)
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
- falhar_se_vazio: bool = False
156
- ) -> dict[str, Optional[str]]:
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 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.
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
- dict: Mapeamento {nome_extracao: caminho_arquivo} para cada extração bem-sucedida.
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
- >>> 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
- >>>
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
- resultados = {}
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
- 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
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
- 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")
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 resultados
352
+ return lote