nia-etl-utils 0.1.0__py3-none-any.whl → 0.2.1__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 +173 -43
- 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 +394 -0
- nia_etl_utils/limpeza_pastas.py +192 -59
- nia_etl_utils/logger_config.py +98 -40
- nia_etl_utils/ocr.py +401 -0
- 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.2.1.dist-info/METADATA +723 -0
- nia_etl_utils-0.2.1.dist-info/RECORD +16 -0
- {nia_etl_utils-0.1.0.dist-info → nia_etl_utils-0.2.1.dist-info}/WHEEL +1 -1
- nia_etl_utils-0.1.0.dist-info/METADATA +0 -594
- nia_etl_utils-0.1.0.dist-info/RECORD +0 -12
- {nia_etl_utils-0.1.0.dist-info → nia_etl_utils-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,30 +1,80 @@
|
|
|
1
|
-
"""Processamento paralelo de arquivos CSV grandes.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Processamento paralelo de arquivos CSV grandes.
|
|
2
|
+
|
|
3
|
+
Fornece funções para processar arquivos CSV em chunks paralelos,
|
|
4
|
+
otimizando o uso de CPU para transformações em arquivos grandes.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Processamento básico:
|
|
8
|
+
|
|
9
|
+
>>> from nia_etl_utils import processar_csv_paralelo
|
|
10
|
+
>>>
|
|
11
|
+
>>> def limpar_texto(texto):
|
|
12
|
+
... if pd.isna(texto):
|
|
13
|
+
... return texto
|
|
14
|
+
... return texto.strip().upper()
|
|
15
|
+
>>>
|
|
16
|
+
>>> processar_csv_paralelo(
|
|
17
|
+
... caminho_entrada="dados_brutos.csv",
|
|
18
|
+
... caminho_saida="dados_limpos.csv",
|
|
19
|
+
... colunas_para_tratar=["nome", "descricao"],
|
|
20
|
+
... funcao_transformacao=limpar_texto
|
|
21
|
+
... )
|
|
22
|
+
|
|
23
|
+
Com configurações customizadas:
|
|
24
|
+
|
|
25
|
+
>>> processar_csv_paralelo(
|
|
26
|
+
... caminho_entrada="arquivo_grande.csv",
|
|
27
|
+
... caminho_saida="arquivo_processado.csv",
|
|
28
|
+
... colunas_para_tratar=["texto"],
|
|
29
|
+
... funcao_transformacao=minha_funcao,
|
|
30
|
+
... chunksize=5000,
|
|
31
|
+
... num_processos=4,
|
|
32
|
+
... remover_entrada=True
|
|
33
|
+
... )
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from collections.abc import Callable
|
|
5
37
|
from multiprocessing import Pool, cpu_count
|
|
38
|
+
from pathlib import Path
|
|
6
39
|
|
|
7
40
|
import pandas as pd
|
|
8
41
|
from loguru import logger
|
|
9
42
|
|
|
43
|
+
from .exceptions import LeituraArquivoError, ProcessamentoError
|
|
44
|
+
|
|
10
45
|
|
|
11
46
|
def calcular_chunksize(caminho_arquivo: str) -> int:
|
|
12
47
|
"""Calcula tamanho ideal de chunk baseado no tamanho do arquivo.
|
|
13
48
|
|
|
49
|
+
Retorna um tamanho de chunk otimizado para balancear uso de memória
|
|
50
|
+
e eficiência de processamento paralelo.
|
|
51
|
+
|
|
14
52
|
Args:
|
|
15
53
|
caminho_arquivo: Caminho do arquivo CSV.
|
|
16
54
|
|
|
17
55
|
Returns:
|
|
18
|
-
|
|
56
|
+
Tamanho do chunk em número de linhas:
|
|
57
|
+
- Arquivo < 500MB: 10000 linhas
|
|
58
|
+
- Arquivo 500MB-2GB: 5000 linhas
|
|
59
|
+
- Arquivo 2-5GB: 2000 linhas
|
|
60
|
+
- Arquivo > 5GB: 1000 linhas
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
LeituraArquivoError: Se o arquivo não existir.
|
|
19
64
|
|
|
20
65
|
Examples:
|
|
21
66
|
>>> chunksize = calcular_chunksize("dados_grandes.csv")
|
|
22
|
-
>>>
|
|
23
|
-
>>> # Arquivo 500MB-2GB: 5000 linhas
|
|
24
|
-
>>> # Arquivo 2-5GB: 2000 linhas
|
|
25
|
-
>>> # Arquivo > 5GB: 1000 linhas
|
|
67
|
+
>>> print(f"Usando chunks de {chunksize} linhas")
|
|
26
68
|
"""
|
|
27
|
-
|
|
69
|
+
arquivo = Path(caminho_arquivo)
|
|
70
|
+
|
|
71
|
+
if not arquivo.exists():
|
|
72
|
+
raise LeituraArquivoError(
|
|
73
|
+
f"Arquivo não encontrado: {caminho_arquivo}",
|
|
74
|
+
details={"caminho": caminho_arquivo}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
tamanho_mb = arquivo.stat().st_size / (1024 * 1024)
|
|
28
78
|
|
|
29
79
|
if tamanho_mb < 500:
|
|
30
80
|
return 10000
|
|
@@ -39,7 +89,15 @@ def calcular_chunksize(caminho_arquivo: str) -> int:
|
|
|
39
89
|
def _processar_chunk(args: tuple) -> pd.DataFrame:
|
|
40
90
|
"""Processa um chunk aplicando transformações.
|
|
41
91
|
|
|
42
|
-
Função interna usada pelo Pool.imap().
|
|
92
|
+
Função interna usada pelo Pool.imap(). Não deve ser chamada
|
|
93
|
+
diretamente.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
args: Tupla contendo (chunk, colunas_para_tratar,
|
|
97
|
+
func_tratar_texto, normalizar_colunas).
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
DataFrame com transformações aplicadas.
|
|
43
101
|
"""
|
|
44
102
|
chunk, colunas_para_tratar, func_tratar_texto, normalizar_colunas = args
|
|
45
103
|
|
|
@@ -60,48 +118,85 @@ def _processar_chunk(args: tuple) -> pd.DataFrame:
|
|
|
60
118
|
def processar_csv_paralelo(
|
|
61
119
|
caminho_entrada: str,
|
|
62
120
|
caminho_saida: str,
|
|
63
|
-
colunas_para_tratar:
|
|
121
|
+
colunas_para_tratar: list[str],
|
|
64
122
|
funcao_transformacao: Callable,
|
|
65
|
-
chunksize:
|
|
123
|
+
chunksize: int | None = None,
|
|
66
124
|
normalizar_colunas: bool = True,
|
|
67
125
|
remover_entrada: bool = False,
|
|
68
|
-
num_processos:
|
|
69
|
-
) ->
|
|
126
|
+
num_processos: int | None = None
|
|
127
|
+
) -> int:
|
|
70
128
|
"""Processa CSV grande em paralelo aplicando transformações por chunk.
|
|
71
129
|
|
|
130
|
+
Lê o arquivo CSV em chunks, processa cada chunk em paralelo usando
|
|
131
|
+
multiprocessing, e escreve o resultado no arquivo de saída.
|
|
132
|
+
|
|
72
133
|
Args:
|
|
73
|
-
caminho_entrada:
|
|
74
|
-
caminho_saida:
|
|
75
|
-
colunas_para_tratar: Lista de colunas para aplicar
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
134
|
+
caminho_entrada: Caminho do arquivo CSV de entrada.
|
|
135
|
+
caminho_saida: Caminho do arquivo CSV de saída.
|
|
136
|
+
colunas_para_tratar: Lista de nomes de colunas para aplicar
|
|
137
|
+
a função de transformação.
|
|
138
|
+
funcao_transformacao: Função que recebe um valor e retorna
|
|
139
|
+
o valor transformado. Deve tratar valores nulos (None/NaN).
|
|
140
|
+
chunksize: Número de linhas por chunk. Se None, calcula
|
|
141
|
+
automaticamente baseado no tamanho do arquivo.
|
|
142
|
+
normalizar_colunas: Se True, converte nomes de colunas para
|
|
143
|
+
lowercase. Defaults to True.
|
|
144
|
+
remover_entrada: Se True, remove arquivo de entrada após
|
|
145
|
+
processar com sucesso. Defaults to False.
|
|
146
|
+
num_processos: Número de processos paralelos. Se None, usa
|
|
147
|
+
o número de CPUs disponíveis.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Número total de linhas processadas.
|
|
81
151
|
|
|
82
152
|
Raises:
|
|
83
|
-
|
|
153
|
+
LeituraArquivoError: Se arquivo de entrada não existir.
|
|
154
|
+
ProcessamentoError: Se houver erro durante o processamento.
|
|
84
155
|
|
|
85
156
|
Examples:
|
|
86
|
-
|
|
87
|
-
|
|
157
|
+
Processamento básico:
|
|
158
|
+
|
|
88
159
|
>>> def limpar_texto(texto):
|
|
160
|
+
... if pd.isna(texto):
|
|
161
|
+
... return texto
|
|
89
162
|
... return texto.strip().upper()
|
|
90
163
|
>>>
|
|
91
|
-
>>> processar_csv_paralelo(
|
|
164
|
+
>>> linhas = processar_csv_paralelo(
|
|
92
165
|
... caminho_entrada="dados_brutos.csv",
|
|
93
166
|
... caminho_saida="dados_limpos.csv",
|
|
94
167
|
... colunas_para_tratar=["nome", "descricao"],
|
|
95
|
-
... funcao_transformacao=limpar_texto
|
|
168
|
+
... funcao_transformacao=limpar_texto
|
|
169
|
+
... )
|
|
170
|
+
>>> print(f"{linhas} linhas processadas")
|
|
171
|
+
|
|
172
|
+
Com configurações customizadas:
|
|
173
|
+
|
|
174
|
+
>>> linhas = processar_csv_paralelo(
|
|
175
|
+
... caminho_entrada="arquivo_grande.csv",
|
|
176
|
+
... caminho_saida="arquivo_processado.csv",
|
|
177
|
+
... colunas_para_tratar=["texto"],
|
|
178
|
+
... funcao_transformacao=minha_funcao,
|
|
179
|
+
... chunksize=5000,
|
|
180
|
+
... num_processos=4,
|
|
96
181
|
... remover_entrada=True
|
|
97
182
|
... )
|
|
183
|
+
|
|
184
|
+
Tratando erros:
|
|
185
|
+
|
|
186
|
+
>>> from nia_etl_utils.exceptions import ProcessamentoError
|
|
187
|
+
>>> try:
|
|
188
|
+
... processar_csv_paralelo(...)
|
|
189
|
+
... except ProcessamentoError as e:
|
|
190
|
+
... logger.error(f"Falha no processamento: {e}")
|
|
98
191
|
"""
|
|
99
192
|
caminho_entrada_path = Path(caminho_entrada)
|
|
100
193
|
|
|
101
194
|
# Validação de entrada
|
|
102
195
|
if not caminho_entrada_path.exists():
|
|
103
|
-
|
|
104
|
-
|
|
196
|
+
raise LeituraArquivoError(
|
|
197
|
+
f"Arquivo de entrada não encontrado: {caminho_entrada}",
|
|
198
|
+
details={"caminho": caminho_entrada}
|
|
199
|
+
)
|
|
105
200
|
|
|
106
201
|
try:
|
|
107
202
|
logger.info(f"Iniciando processamento paralelo: {caminho_entrada}")
|
|
@@ -110,12 +205,14 @@ def processar_csv_paralelo(
|
|
|
110
205
|
if chunksize is None:
|
|
111
206
|
chunksize = calcular_chunksize(caminho_entrada)
|
|
112
207
|
|
|
113
|
-
|
|
208
|
+
processos = num_processos or cpu_count()
|
|
209
|
+
logger.info(f"Chunksize: {chunksize} linhas | Processos: {processos}")
|
|
114
210
|
|
|
115
211
|
# Processamento paralelo
|
|
116
212
|
primeiro_chunk = True
|
|
213
|
+
total_linhas = 0
|
|
117
214
|
|
|
118
|
-
with Pool(processes=
|
|
215
|
+
with Pool(processes=processos) as pool:
|
|
119
216
|
reader = pd.read_csv(caminho_entrada, chunksize=chunksize)
|
|
120
217
|
|
|
121
218
|
# Prepara tasks para processamento paralelo
|
|
@@ -126,7 +223,9 @@ def processar_csv_paralelo(
|
|
|
126
223
|
|
|
127
224
|
# Processa chunks em paralelo
|
|
128
225
|
for i, chunk_processado in enumerate(pool.imap(_processar_chunk, tasks), start=1):
|
|
129
|
-
|
|
226
|
+
linhas_chunk = len(chunk_processado)
|
|
227
|
+
total_linhas += linhas_chunk
|
|
228
|
+
logger.debug(f"Escrevendo chunk {i} ({linhas_chunk} linhas)")
|
|
130
229
|
|
|
131
230
|
chunk_processado.to_csv(
|
|
132
231
|
caminho_saida,
|
|
@@ -136,7 +235,7 @@ def processar_csv_paralelo(
|
|
|
136
235
|
)
|
|
137
236
|
primeiro_chunk = False
|
|
138
237
|
|
|
139
|
-
logger.success(f"Processamento concluído: {caminho_saida}")
|
|
238
|
+
logger.success(f"Processamento concluído: {caminho_saida} ({total_linhas} linhas)")
|
|
140
239
|
|
|
141
240
|
# Remove arquivo de entrada se solicitado
|
|
142
241
|
if remover_entrada:
|
|
@@ -146,6 +245,20 @@ def processar_csv_paralelo(
|
|
|
146
245
|
except Exception as e:
|
|
147
246
|
logger.warning(f"Falha ao remover arquivo de entrada: {e}")
|
|
148
247
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
248
|
+
return total_linhas
|
|
249
|
+
|
|
250
|
+
except pd.errors.EmptyDataError as e:
|
|
251
|
+
raise ProcessamentoError(
|
|
252
|
+
f"Arquivo de entrada está vazio: {caminho_entrada}",
|
|
253
|
+
details={"caminho": caminho_entrada, "erro": str(e)}
|
|
254
|
+
) from e
|
|
255
|
+
except pd.errors.ParserError as e:
|
|
256
|
+
raise ProcessamentoError(
|
|
257
|
+
f"Erro ao parsear CSV: {caminho_entrada}",
|
|
258
|
+
details={"caminho": caminho_entrada, "erro": str(e)}
|
|
259
|
+
) from e
|
|
260
|
+
except Exception as e:
|
|
261
|
+
raise ProcessamentoError(
|
|
262
|
+
f"Erro no processamento paralelo: {caminho_entrada}",
|
|
263
|
+
details={"caminho": caminho_entrada, "erro": str(e)}
|
|
264
|
+
) from e
|
nia_etl_utils/results.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Dataclasses de resultado para operações do pacote nia_etl_utils.
|
|
2
|
+
|
|
3
|
+
Este módulo define estruturas de dados para retorno de operações,
|
|
4
|
+
fornecendo informações estruturadas sobre o resultado de cada ação.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Resultado de extração:
|
|
8
|
+
|
|
9
|
+
>>> resultado = ResultadoExtracao(
|
|
10
|
+
... nome="clientes",
|
|
11
|
+
... caminho="/tmp/clientes_2025_01_20.csv",
|
|
12
|
+
... linhas=1500,
|
|
13
|
+
... sucesso=True
|
|
14
|
+
... )
|
|
15
|
+
>>> if resultado.sucesso:
|
|
16
|
+
... print(f"Exportados {resultado.linhas} registros")
|
|
17
|
+
|
|
18
|
+
Resultado com erro:
|
|
19
|
+
|
|
20
|
+
>>> resultado = ResultadoExtracao(
|
|
21
|
+
... nome="vendas",
|
|
22
|
+
... caminho=None,
|
|
23
|
+
... linhas=0,
|
|
24
|
+
... sucesso=False,
|
|
25
|
+
... erro="Nenhum dado retornado"
|
|
26
|
+
... )
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Conexao:
|
|
35
|
+
"""Wrapper para conexão de banco de dados.
|
|
36
|
+
|
|
37
|
+
Encapsula cursor e connection, fornecendo interface consistente
|
|
38
|
+
e suporte a context manager para fechamento automático.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
cursor: Cursor ativo para execução de queries.
|
|
42
|
+
connection: Objeto de conexão subjacente (psycopg2 ou cx_Oracle).
|
|
43
|
+
database: Nome/identificador do banco conectado.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
Uso com context manager (recomendado):
|
|
47
|
+
|
|
48
|
+
>>> with conectar_postgresql(config) as conn:
|
|
49
|
+
... conn.cursor.execute("SELECT * FROM tabela")
|
|
50
|
+
... dados = conn.cursor.fetchall()
|
|
51
|
+
... # conexão fechada automaticamente
|
|
52
|
+
|
|
53
|
+
Uso manual:
|
|
54
|
+
|
|
55
|
+
>>> conn = conectar_postgresql(config)
|
|
56
|
+
>>> try:
|
|
57
|
+
... conn.cursor.execute("SELECT 1")
|
|
58
|
+
... resultado = conn.cursor.fetchone()
|
|
59
|
+
... finally:
|
|
60
|
+
... conn.fechar()
|
|
61
|
+
|
|
62
|
+
Acesso aos componentes:
|
|
63
|
+
|
|
64
|
+
>>> conn = conectar_postgresql(config)
|
|
65
|
+
>>> conn.cursor.execute("SELECT COUNT(*) FROM usuarios")
|
|
66
|
+
>>> total = conn.cursor.fetchone()[0]
|
|
67
|
+
>>> conn.connection.commit() # se necessário
|
|
68
|
+
>>> conn.fechar()
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
cursor: Any
|
|
72
|
+
connection: Any
|
|
73
|
+
database: str
|
|
74
|
+
|
|
75
|
+
def fechar(self) -> None:
|
|
76
|
+
"""Encerra cursor e conexão de forma segura.
|
|
77
|
+
|
|
78
|
+
Tenta fechar cursor e conexão, logando warnings em caso
|
|
79
|
+
de erro mas nunca levantando exceções.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> conn = conectar_postgresql(config)
|
|
83
|
+
>>> # ... usar conexão ...
|
|
84
|
+
>>> conn.fechar() # sempre seguro
|
|
85
|
+
"""
|
|
86
|
+
from loguru import logger
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
if self.cursor:
|
|
90
|
+
self.cursor.close()
|
|
91
|
+
logger.debug("Cursor fechado com sucesso.")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.warning(f"Erro ao fechar cursor: {e}")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if self.connection:
|
|
97
|
+
self.connection.close()
|
|
98
|
+
logger.debug("Conexão encerrada com sucesso.")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.warning(f"Erro ao fechar conexão: {e}")
|
|
101
|
+
|
|
102
|
+
def __enter__(self) -> "Conexao":
|
|
103
|
+
"""Entrada do context manager."""
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
107
|
+
"""Saída do context manager - fecha conexão automaticamente."""
|
|
108
|
+
self.fechar()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class ResultadoExtracao:
|
|
113
|
+
"""Resultado de uma operação de extração e exportação CSV.
|
|
114
|
+
|
|
115
|
+
Fornece informações estruturadas sobre o resultado de uma
|
|
116
|
+
extração, incluindo métricas e status de sucesso/erro.
|
|
117
|
+
|
|
118
|
+
Attributes:
|
|
119
|
+
nome: Identificador da extração.
|
|
120
|
+
caminho: Caminho do arquivo CSV gerado (None se falhou).
|
|
121
|
+
linhas: Quantidade de registros extraídos.
|
|
122
|
+
sucesso: True se a operação completou com sucesso.
|
|
123
|
+
erro: Mensagem de erro se sucesso=False, None caso contrário.
|
|
124
|
+
colunas: Quantidade de colunas no DataFrame (opcional).
|
|
125
|
+
tamanho_bytes: Tamanho do arquivo em bytes (opcional).
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
Extração bem-sucedida:
|
|
129
|
+
|
|
130
|
+
>>> resultado = ResultadoExtracao(
|
|
131
|
+
... nome="clientes",
|
|
132
|
+
... caminho="/tmp/clientes_2025_01_20.csv",
|
|
133
|
+
... linhas=1500,
|
|
134
|
+
... sucesso=True,
|
|
135
|
+
... colunas=12,
|
|
136
|
+
... tamanho_bytes=45000
|
|
137
|
+
... )
|
|
138
|
+
>>> print(f"Exportados {resultado.linhas} registros para {resultado.caminho}")
|
|
139
|
+
|
|
140
|
+
Extração com falha:
|
|
141
|
+
|
|
142
|
+
>>> resultado = ResultadoExtracao(
|
|
143
|
+
... nome="vendas",
|
|
144
|
+
... caminho=None,
|
|
145
|
+
... linhas=0,
|
|
146
|
+
... sucesso=False,
|
|
147
|
+
... erro="Nenhum dado retornado para extração 'vendas'"
|
|
148
|
+
... )
|
|
149
|
+
>>> if not resultado.sucesso:
|
|
150
|
+
... logger.warning(resultado.erro)
|
|
151
|
+
|
|
152
|
+
Verificando resultados em lote:
|
|
153
|
+
|
|
154
|
+
>>> resultados = exportar_multiplos_csv(extractions, ...)
|
|
155
|
+
>>> sucesso = [r for r in resultados if r.sucesso]
|
|
156
|
+
>>> falhas = [r for r in resultados if not r.sucesso]
|
|
157
|
+
>>> print(f"{len(sucesso)} OK, {len(falhas)} falhas")
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
nome: str
|
|
161
|
+
caminho: str | None
|
|
162
|
+
linhas: int
|
|
163
|
+
sucesso: bool
|
|
164
|
+
erro: str | None = None
|
|
165
|
+
colunas: int | None = None
|
|
166
|
+
tamanho_bytes: int | None = None
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def tamanho_kb(self) -> float | None:
|
|
170
|
+
"""Tamanho do arquivo em kilobytes.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Tamanho em KB ou None se tamanho_bytes não definido.
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
>>> resultado.tamanho_bytes = 45000
|
|
177
|
+
>>> resultado.tamanho_kb
|
|
178
|
+
43.945...
|
|
179
|
+
"""
|
|
180
|
+
if self.tamanho_bytes is None:
|
|
181
|
+
return None
|
|
182
|
+
return self.tamanho_bytes / 1024
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def tamanho_mb(self) -> float | None:
|
|
186
|
+
"""Tamanho do arquivo em megabytes.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Tamanho em MB ou None se tamanho_bytes não definido.
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> resultado.tamanho_bytes = 1048576
|
|
193
|
+
>>> resultado.tamanho_mb
|
|
194
|
+
1.0
|
|
195
|
+
"""
|
|
196
|
+
if self.tamanho_bytes is None:
|
|
197
|
+
return None
|
|
198
|
+
return self.tamanho_bytes / (1024 * 1024)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class ResultadoLote:
|
|
203
|
+
"""Resultado consolidado de operações em lote.
|
|
204
|
+
|
|
205
|
+
Agrupa múltiplos ResultadoExtracao e fornece métricas
|
|
206
|
+
consolidadas sobre o lote.
|
|
207
|
+
|
|
208
|
+
Attributes:
|
|
209
|
+
resultados: Lista de ResultadoExtracao individuais.
|
|
210
|
+
total: Número total de extrações no lote.
|
|
211
|
+
sucesso: Número de extrações bem-sucedidas.
|
|
212
|
+
falhas: Número de extrações que falharam.
|
|
213
|
+
|
|
214
|
+
Examples:
|
|
215
|
+
>>> lote = ResultadoLote(resultados=[r1, r2, r3])
|
|
216
|
+
>>> print(f"Taxa de sucesso: {lote.taxa_sucesso:.1%}")
|
|
217
|
+
>>> if lote.todos_sucesso:
|
|
218
|
+
... print("Todas extrações OK!")
|
|
219
|
+
>>> for falha in lote.extrações_falhas:
|
|
220
|
+
... print(f"Falhou: {falha.nome} - {falha.erro}")
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
resultados: list[ResultadoExtracao] = field(default_factory=list)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def total(self) -> int:
|
|
227
|
+
"""Número total de extrações no lote."""
|
|
228
|
+
return len(self.resultados)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def sucesso(self) -> int:
|
|
232
|
+
"""Número de extrações bem-sucedidas."""
|
|
233
|
+
return sum(1 for r in self.resultados if r.sucesso)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def falhas(self) -> int:
|
|
237
|
+
"""Número de extrações que falharam."""
|
|
238
|
+
return sum(1 for r in self.resultados if not r.sucesso)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def todos_sucesso(self) -> bool:
|
|
242
|
+
"""True se todas as extrações foram bem-sucedidas."""
|
|
243
|
+
return self.falhas == 0
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def taxa_sucesso(self) -> float:
|
|
247
|
+
"""Taxa de sucesso (0.0 a 1.0)."""
|
|
248
|
+
if self.total == 0:
|
|
249
|
+
return 0.0
|
|
250
|
+
return self.sucesso / self.total
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def extracoes_sucesso(self) -> list[ResultadoExtracao]:
|
|
254
|
+
"""Lista de extrações bem-sucedidas."""
|
|
255
|
+
return [r for r in self.resultados if r.sucesso]
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def extracoes_falhas(self) -> list[ResultadoExtracao]:
|
|
259
|
+
"""Lista de extrações que falharam."""
|
|
260
|
+
return [r for r in self.resultados if not r.sucesso]
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def total_linhas(self) -> int:
|
|
264
|
+
"""Total de linhas extraídas em todas as extrações."""
|
|
265
|
+
return sum(r.linhas for r in self.resultados)
|
|
266
|
+
|
|
267
|
+
def adicionar(self, resultado: ResultadoExtracao) -> None:
|
|
268
|
+
"""Adiciona um resultado ao lote.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
resultado: ResultadoExtracao a ser adicionado.
|
|
272
|
+
|
|
273
|
+
Examples:
|
|
274
|
+
>>> lote = ResultadoLote()
|
|
275
|
+
>>> lote.adicionar(resultado1)
|
|
276
|
+
>>> lote.adicionar(resultado2)
|
|
277
|
+
"""
|
|
278
|
+
self.resultados.append(resultado)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class ResultadoEmail:
|
|
283
|
+
"""Resultado de envio de email.
|
|
284
|
+
|
|
285
|
+
Attributes:
|
|
286
|
+
sucesso: True se o email foi enviado com sucesso.
|
|
287
|
+
destinatarios: Lista de destinatários do email.
|
|
288
|
+
assunto: Assunto do email enviado.
|
|
289
|
+
erro: Mensagem de erro se sucesso=False.
|
|
290
|
+
anexo: Caminho do anexo enviado (se houver).
|
|
291
|
+
|
|
292
|
+
Examples:
|
|
293
|
+
>>> resultado = ResultadoEmail(
|
|
294
|
+
... sucesso=True,
|
|
295
|
+
... destinatarios=["admin@empresa.com"],
|
|
296
|
+
... assunto="Relatório Diário"
|
|
297
|
+
... )
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
sucesso: bool
|
|
301
|
+
destinatarios: list[str]
|
|
302
|
+
assunto: str
|
|
303
|
+
erro: str | None = None
|
|
304
|
+
anexo: str | None = None
|