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.
@@ -0,0 +1,327 @@
1
+ """Exceções customizadas para o pacote nia_etl_utils.
2
+
3
+ Este módulo define a hierarquia de exceções usada em todo o pacote,
4
+ permitindo tratamento granular de erros pelos consumidores da biblioteca.
5
+
6
+ Hierarquia:
7
+ NiaEtlError (base)
8
+ ├── ConfiguracaoError
9
+ │ └── VariavelAmbienteError
10
+ ├── DatabaseError
11
+ │ └── ConexaoError
12
+ ├── ArquivoError
13
+ │ ├── EscritaArquivoError
14
+ │ ├── LeituraArquivoError
15
+ │ └── DiretorioError
16
+ ├── ExtracaoError
17
+ │ └── ExtracaoVaziaError
18
+ ├── EmailError
19
+ │ ├── DestinatarioError
20
+ │ └── SmtpError
21
+ └── ValidacaoError
22
+
23
+ Examples:
24
+ Capturando erros específicos:
25
+
26
+ >>> from nia_etl_utils.exceptions import ConexaoError, ExtracaoVaziaError
27
+ >>>
28
+ >>> try:
29
+ ... conn = conectar_postgresql(config)
30
+ ... except ConexaoError as e:
31
+ ... logger.error(f"Falha na conexão: {e}")
32
+ ... # tratamento específico
33
+
34
+ Capturando qualquer erro do pacote:
35
+
36
+ >>> from nia_etl_utils.exceptions import NiaEtlError
37
+ >>>
38
+ >>> try:
39
+ ... executar_pipeline()
40
+ ... except NiaEtlError as e:
41
+ ... logger.error(f"Erro no pipeline: {e}")
42
+ ... sys.exit(1) # decisão do CHAMADOR
43
+ """
44
+
45
+
46
+ class NiaEtlError(Exception):
47
+ """Exceção base para todos os erros do pacote nia_etl_utils.
48
+
49
+ Todas as exceções customizadas do pacote herdam desta classe,
50
+ permitindo captura genérica quando necessário.
51
+
52
+ Attributes:
53
+ message: Descrição do erro.
54
+ details: Informações adicionais opcionais sobre o erro.
55
+
56
+ Examples:
57
+ >>> try:
58
+ ... raise NiaEtlError("Algo deu errado", details={"codigo": 123})
59
+ ... except NiaEtlError as e:
60
+ ... print(e.message)
61
+ ... print(e.details)
62
+ Algo deu errado
63
+ {'codigo': 123}
64
+ """
65
+
66
+ def __init__(self, message: str, details: dict | None = None):
67
+ self.message = message
68
+ self.details = details or {}
69
+ super().__init__(self.message)
70
+
71
+ def __str__(self) -> str:
72
+ if self.details:
73
+ return f"{self.message} | Detalhes: {self.details}"
74
+ return self.message
75
+
76
+
77
+ # =============================================================================
78
+ # CONFIGURAÇÃO
79
+ # =============================================================================
80
+
81
+
82
+ class ConfiguracaoError(NiaEtlError):
83
+ """Erro de configuração do sistema.
84
+
85
+ Levantado quando há problemas com configurações necessárias
86
+ para o funcionamento do pacote.
87
+
88
+ Examples:
89
+ >>> raise ConfiguracaoError("Configuração inválida para conexão")
90
+ """
91
+
92
+ pass
93
+
94
+
95
+ class VariavelAmbienteError(ConfiguracaoError):
96
+ """Variável de ambiente ausente ou inválida.
97
+
98
+ Levantado quando uma variável de ambiente obrigatória não está
99
+ definida e nenhum valor padrão foi fornecido.
100
+
101
+ Attributes:
102
+ nome_variavel: Nome da variável de ambiente que causou o erro.
103
+
104
+ Examples:
105
+ >>> raise VariavelAmbienteError("DB_HOST")
106
+ """
107
+
108
+ def __init__(self, nome_variavel: str):
109
+ self.nome_variavel = nome_variavel
110
+ super().__init__(
111
+ f"Variável de ambiente '{nome_variavel}' não encontrada "
112
+ f"e nenhum valor padrão foi fornecido",
113
+ details={"variavel": nome_variavel}
114
+ )
115
+
116
+
117
+ # =============================================================================
118
+ # DATABASE
119
+ # =============================================================================
120
+
121
+
122
+ class DatabaseError(NiaEtlError):
123
+ """Erro base para operações de banco de dados.
124
+
125
+ Examples:
126
+ >>> raise DatabaseError("Falha na operação de banco de dados")
127
+ """
128
+
129
+ pass
130
+
131
+
132
+ class ConexaoError(DatabaseError):
133
+ """Falha ao estabelecer conexão com banco de dados.
134
+
135
+ Levantado quando não é possível conectar ao banco de dados,
136
+ seja por credenciais inválidas, host inacessível ou outros
137
+ problemas de conectividade.
138
+
139
+ Examples:
140
+ >>> raise ConexaoError(
141
+ ... "Timeout ao conectar",
142
+ ... details={"host": "localhost", "port": 5432}
143
+ ... )
144
+ """
145
+
146
+ pass
147
+
148
+
149
+ # =============================================================================
150
+ # ARQUIVOS E DIRETÓRIOS
151
+ # =============================================================================
152
+
153
+
154
+ class ArquivoError(NiaEtlError):
155
+ """Erro base para operações de arquivo e diretório.
156
+
157
+ Examples:
158
+ >>> raise ArquivoError("Operação de arquivo falhou")
159
+ """
160
+
161
+ pass
162
+
163
+
164
+ class EscritaArquivoError(ArquivoError):
165
+ """Falha ao escrever arquivo.
166
+
167
+ Levantado quando não é possível criar ou escrever em um arquivo,
168
+ seja por falta de permissão, disco cheio ou caminho inválido.
169
+
170
+ Examples:
171
+ >>> raise EscritaArquivoError(
172
+ ... "Sem permissão para escrita",
173
+ ... details={"caminho": "/etc/arquivo.csv"}
174
+ ... )
175
+ """
176
+
177
+ pass
178
+
179
+
180
+ class LeituraArquivoError(ArquivoError):
181
+ """Falha ao ler arquivo.
182
+
183
+ Levantado quando não é possível ler um arquivo, seja porque
184
+ ele não existe, não há permissão ou está corrompido.
185
+
186
+ Examples:
187
+ >>> raise LeituraArquivoError(
188
+ ... "Arquivo não encontrado",
189
+ ... details={"caminho": "/tmp/dados.csv"}
190
+ ... )
191
+ """
192
+
193
+ pass
194
+
195
+
196
+ class DiretorioError(ArquivoError):
197
+ """Falha em operação de diretório.
198
+
199
+ Levantado quando não é possível criar, limpar ou remover
200
+ um diretório.
201
+
202
+ Examples:
203
+ >>> raise DiretorioError(
204
+ ... "Sem permissão para criar diretório",
205
+ ... details={"caminho": "/root/dados"}
206
+ ... )
207
+ """
208
+
209
+ pass
210
+
211
+
212
+ # =============================================================================
213
+ # EXTRAÇÃO E PROCESSAMENTO
214
+ # =============================================================================
215
+
216
+
217
+ class ExtracaoError(NiaEtlError):
218
+ """Erro base para operações de extração de dados.
219
+
220
+ Examples:
221
+ >>> raise ExtracaoError("Falha na extração de dados")
222
+ """
223
+
224
+ pass
225
+
226
+
227
+ class ExtracaoVaziaError(ExtracaoError):
228
+ """Extração retornou DataFrame vazio ou None.
229
+
230
+ Levantado quando uma função de extração não retorna dados.
231
+ Pode ser esperado em alguns contextos (extração incremental
232
+ sem novos dados) ou indicar um problema.
233
+
234
+ Attributes:
235
+ nome_extracao: Identificador da extração que falhou.
236
+
237
+ Examples:
238
+ >>> raise ExtracaoVaziaError("clientes_novos")
239
+ """
240
+
241
+ def __init__(self, nome_extracao: str):
242
+ self.nome_extracao = nome_extracao
243
+ super().__init__(
244
+ f"Nenhum dado retornado para extração '{nome_extracao}'",
245
+ details={"extracao": nome_extracao}
246
+ )
247
+
248
+
249
+ class ProcessamentoError(ExtracaoError):
250
+ """Erro durante processamento de dados.
251
+
252
+ Levantado quando há falha durante transformação ou
253
+ processamento de dados.
254
+
255
+ Examples:
256
+ >>> raise ProcessamentoError(
257
+ ... "Falha ao processar chunk",
258
+ ... details={"chunk": 5, "erro": "memória insuficiente"}
259
+ ... )
260
+ """
261
+
262
+ pass
263
+
264
+
265
+ # =============================================================================
266
+ # EMAIL
267
+ # =============================================================================
268
+
269
+
270
+ class EmailError(NiaEtlError):
271
+ """Erro base para operações de email.
272
+
273
+ Examples:
274
+ >>> raise EmailError("Falha no envio de email")
275
+ """
276
+
277
+ pass
278
+
279
+
280
+ class DestinatarioError(EmailError):
281
+ """Erro relacionado a destinatários de email.
282
+
283
+ Levantado quando não há destinatários configurados ou
284
+ quando os destinatários são inválidos.
285
+
286
+ Examples:
287
+ >>> raise DestinatarioError("Nenhum destinatário configurado")
288
+ """
289
+
290
+ pass
291
+
292
+
293
+ class SmtpError(EmailError):
294
+ """Erro de comunicação com servidor SMTP.
295
+
296
+ Levantado quando há falha na conexão ou comunicação
297
+ com o servidor de email.
298
+
299
+ Examples:
300
+ >>> raise SmtpError(
301
+ ... "Conexão recusada",
302
+ ... details={"servidor": "smtp.empresa.com", "porta": 587}
303
+ ... )
304
+ """
305
+
306
+ pass
307
+
308
+
309
+ # =============================================================================
310
+ # VALIDAÇÃO
311
+ # =============================================================================
312
+
313
+
314
+ class ValidacaoError(NiaEtlError):
315
+ """Erro de validação de parâmetros ou dados.
316
+
317
+ Levantado quando parâmetros fornecidos são inválidos
318
+ ou não atendem aos requisitos esperados.
319
+
320
+ Examples:
321
+ >>> raise ValidacaoError(
322
+ ... "Nome do arquivo não pode ser vazio",
323
+ ... details={"parametro": "nome_arquivo", "valor": ""}
324
+ ... )
325
+ """
326
+
327
+ pass
@@ -1,12 +1,35 @@
1
- """Funções utilitárias para manipulação de arquivos e diretórios."""
2
- import sys
1
+ """Funções utilitárias para manipulação de arquivos e diretórios.
2
+
3
+ Fornece operações comuns de sistema de arquivos com logging
4
+ apropriado e tratamento de erros consistente.
5
+
6
+ Examples:
7
+ Limpar pasta antes de processamento:
8
+
9
+ >>> from nia_etl_utils import limpar_pasta
10
+ >>> limpar_pasta("/tmp/meu_pipeline")
11
+
12
+ Criar estrutura de diretórios:
13
+
14
+ >>> from nia_etl_utils import criar_pasta_se_nao_existir
15
+ >>> criar_pasta_se_nao_existir("/dados/processados/2025/01")
16
+
17
+ Remover pasta temporária:
18
+
19
+ >>> from nia_etl_utils import remover_pasta_recursivamente
20
+ >>> remover_pasta_recursivamente("/tmp/pasta_temporaria")
21
+ """
22
+
3
23
  import shutil
4
24
  from pathlib import Path
25
+
5
26
  from loguru import logger
6
27
 
28
+ from .exceptions import DiretorioError
29
+
7
30
 
8
- def limpar_pasta(pasta: str, log: bool = True) -> None:
9
- """Remove todos os arquivos de uma pasta, recriando-a se necessário.
31
+ def limpar_pasta(pasta: str, log: bool = True) -> int:
32
+ """Remove todos os arquivos de uma pasta, preservando subdiretórios.
10
33
 
11
34
  Se a pasta não existir, ela será criada. Se existir, todos os arquivos
12
35
  dentro dela serão removidos (subdiretórios são preservados).
@@ -15,13 +38,22 @@ def limpar_pasta(pasta: str, log: bool = True) -> None:
15
38
  pasta: Caminho da pasta que será limpa.
16
39
  log: Se True, emite logs com Loguru. Defaults to True.
17
40
 
41
+ Returns:
42
+ Número de arquivos removidos.
43
+
18
44
  Raises:
19
- SystemExit: Se houver erro ao criar ou limpar a pasta.
45
+ DiretorioError: Se houver erro ao criar ou limpar a pasta.
20
46
 
21
47
  Examples:
22
- >>> from nia_etl_utils.limpeza_pastas import limpar_pasta
23
- >>> limpar_pasta("/tmp/meu_pipeline")
24
- >>> # Pasta criada ou limpa com sucesso
48
+ Limpar pasta de saída antes de processamento:
49
+
50
+ >>> from nia_etl_utils import limpar_pasta
51
+ >>> removidos = limpar_pasta("/tmp/meu_pipeline")
52
+ >>> print(f"{removidos} arquivo(s) removido(s)")
53
+
54
+ Limpar sem logging:
55
+
56
+ >>> limpar_pasta("/tmp/dados", log=False)
25
57
  """
26
58
  try:
27
59
  pasta_path = Path(pasta)
@@ -30,46 +62,66 @@ def limpar_pasta(pasta: str, log: bool = True) -> None:
30
62
  pasta_path.mkdir(parents=True, exist_ok=True)
31
63
  if log:
32
64
  logger.info(f"Pasta criada: {pasta}")
33
- else:
34
- arquivos_removidos = 0
65
+ return 0
35
66
 
36
- for item in pasta_path.iterdir():
37
- if item.is_file():
38
- item.unlink()
39
- arquivos_removidos += 1
40
- if log:
41
- logger.debug(f"Arquivo removido: {item}")
67
+ arquivos_removidos = 0
42
68
 
43
- if log:
44
- logger.info(f"Pasta '{pasta}' limpa com sucesso. {arquivos_removidos} arquivo(s) removido(s).")
69
+ for item in pasta_path.iterdir():
70
+ if item.is_file():
71
+ item.unlink()
72
+ arquivos_removidos += 1
73
+ if log:
74
+ logger.debug(f"Arquivo removido: {item}")
45
75
 
46
- except PermissionError as error:
47
- logger.error(f"Sem permissão para acessar/modificar a pasta '{pasta}': {error}")
48
- sys.exit(1)
49
- except OSError as error:
50
- logger.error(f"Erro do sistema ao manipular a pasta '{pasta}': {error}")
51
- sys.exit(1)
52
- except Exception as error:
53
- logger.error(f"Erro inesperado ao limpar a pasta '{pasta}': {error}")
54
- sys.exit(1)
76
+ if log:
77
+ logger.info(
78
+ f"Pasta '{pasta}' limpa com sucesso. "
79
+ f"{arquivos_removidos} arquivo(s) removido(s)."
80
+ )
55
81
 
82
+ return arquivos_removidos
56
83
 
57
- def remover_pasta_recursivamente(pasta: str, log: bool = True) -> None:
84
+ except PermissionError as e:
85
+ raise DiretorioError(
86
+ f"Sem permissão para acessar/modificar a pasta '{pasta}'",
87
+ details={"pasta": pasta, "erro": str(e)}
88
+ ) from e
89
+ except OSError as e:
90
+ raise DiretorioError(
91
+ f"Erro do sistema ao manipular a pasta '{pasta}'",
92
+ details={"pasta": pasta, "erro": str(e)}
93
+ ) from e
94
+
95
+
96
+ def remover_pasta_recursivamente(pasta: str, log: bool = True) -> bool:
58
97
  """Remove uma pasta e todo seu conteúdo (arquivos e subpastas).
59
98
 
60
- ATENÇÃO: Esta função remove TUDO dentro da pasta, incluindo subdiretórios.
61
- Use com cautela.
99
+ ATENÇÃO: Esta função remove TUDO dentro da pasta, incluindo
100
+ subdiretórios. Use com cautela.
62
101
 
63
102
  Args:
64
103
  pasta: Caminho da pasta que será removida completamente.
65
104
  log: Se True, emite logs com Loguru. Defaults to True.
66
105
 
106
+ Returns:
107
+ True se a pasta foi removida, False se não existia.
108
+
67
109
  Raises:
68
- SystemExit: Se houver erro ao remover a pasta.
110
+ DiretorioError: Se o caminho não for um diretório ou
111
+ houver erro ao remover.
69
112
 
70
113
  Examples:
71
- >>> from nia_etl_utils.limpeza_pastas import remover_pasta_recursivamente
72
- >>> remover_pasta_recursivamente("/tmp/pasta_temporaria")
114
+ Remover pasta temporária:
115
+
116
+ >>> from nia_etl_utils import remover_pasta_recursivamente
117
+ >>> if remover_pasta_recursivamente("/tmp/pasta_temporaria"):
118
+ ... print("Pasta removida")
119
+ ... else:
120
+ ... print("Pasta não existia")
121
+
122
+ Remover sem logging:
123
+
124
+ >>> remover_pasta_recursivamente("/tmp/dados", log=False)
73
125
  """
74
126
  try:
75
127
  pasta_path = Path(pasta)
@@ -77,41 +129,58 @@ def remover_pasta_recursivamente(pasta: str, log: bool = True) -> None:
77
129
  if not pasta_path.exists():
78
130
  if log:
79
131
  logger.warning(f"Pasta '{pasta}' não existe. Nada a remover.")
80
- return
132
+ return False
81
133
 
82
134
  if not pasta_path.is_dir():
83
- logger.error(f"'{pasta}' não é um diretório.")
84
- sys.exit(1)
135
+ raise DiretorioError(
136
+ f"'{pasta}' não é um diretório",
137
+ details={"pasta": pasta, "tipo": "arquivo"}
138
+ )
85
139
 
86
140
  shutil.rmtree(pasta_path)
87
141
 
88
142
  if log:
89
143
  logger.info(f"Pasta '{pasta}' removida completamente (incluindo subpastas).")
90
144
 
91
- except PermissionError as error:
92
- logger.error(f"Sem permissão para remover a pasta '{pasta}': {error}")
93
- sys.exit(1)
94
- except OSError as error:
95
- logger.error(f"Erro do sistema ao remover a pasta '{pasta}': {error}")
96
- sys.exit(1)
97
- except Exception as error:
98
- logger.error(f"Erro inesperado ao remover a pasta '{pasta}': {error}")
99
- sys.exit(1)
145
+ return True
100
146
 
147
+ except PermissionError as e:
148
+ raise DiretorioError(
149
+ f"Sem permissão para remover a pasta '{pasta}'",
150
+ details={"pasta": pasta, "erro": str(e)}
151
+ ) from e
152
+ except OSError as e:
153
+ raise DiretorioError(
154
+ f"Erro do sistema ao remover a pasta '{pasta}'",
155
+ details={"pasta": pasta, "erro": str(e)}
156
+ ) from e
101
157
 
102
- def criar_pasta_se_nao_existir(pasta: str, log: bool = True) -> None:
158
+
159
+ def criar_pasta_se_nao_existir(pasta: str, log: bool = True) -> bool:
103
160
  """Cria uma pasta se ela não existir (incluindo pastas pai).
104
161
 
105
162
  Args:
106
163
  pasta: Caminho da pasta que será criada.
107
164
  log: Se True, emite logs com Loguru. Defaults to True.
108
165
 
166
+ Returns:
167
+ True se a pasta foi criada, False se já existia.
168
+
109
169
  Raises:
110
- SystemExit: Se houver erro ao criar a pasta.
170
+ DiretorioError: Se houver erro ao criar a pasta.
111
171
 
112
172
  Examples:
113
- >>> from nia_etl_utils.limpeza_pastas import criar_pasta_se_nao_existir
114
- >>> criar_pasta_se_nao_existir("/tmp/dados/processados/2025")
173
+ Criar estrutura de diretórios:
174
+
175
+ >>> from nia_etl_utils import criar_pasta_se_nao_existir
176
+ >>> if criar_pasta_se_nao_existir("/tmp/dados/processados/2025"):
177
+ ... print("Estrutura criada")
178
+ ... else:
179
+ ... print("Já existia")
180
+
181
+ Criar sem logging:
182
+
183
+ >>> criar_pasta_se_nao_existir("/tmp/dados", log=False)
115
184
  """
116
185
  try:
117
186
  pasta_path = Path(pasta)
@@ -119,19 +188,83 @@ def criar_pasta_se_nao_existir(pasta: str, log: bool = True) -> None:
119
188
  if pasta_path.exists():
120
189
  if log:
121
190
  logger.debug(f"Pasta '{pasta}' já existe.")
122
- return
191
+ return False
123
192
 
124
193
  pasta_path.mkdir(parents=True, exist_ok=True)
125
194
 
126
195
  if log:
127
196
  logger.info(f"Pasta criada: {pasta}")
128
197
 
129
- except PermissionError as error:
130
- logger.error(f"Sem permissão para criar a pasta '{pasta}': {error}")
131
- sys.exit(1)
132
- except OSError as error:
133
- logger.error(f"Erro do sistema ao criar a pasta '{pasta}': {error}")
134
- sys.exit(1)
135
- except Exception as error:
136
- logger.error(f"Erro inesperado ao criar a pasta '{pasta}': {error}")
137
- sys.exit(1)
198
+ return True
199
+
200
+ except PermissionError as e:
201
+ raise DiretorioError(
202
+ f"Sem permissão para criar a pasta '{pasta}'",
203
+ details={"pasta": pasta, "erro": str(e)}
204
+ ) from e
205
+ except OSError as e:
206
+ raise DiretorioError(
207
+ f"Erro do sistema ao criar a pasta '{pasta}'",
208
+ details={"pasta": pasta, "erro": str(e)}
209
+ ) from e
210
+
211
+
212
+ def listar_arquivos(
213
+ pasta: str,
214
+ extensao: str | None = None,
215
+ recursivo: bool = False
216
+ ) -> list[Path]:
217
+ """Lista arquivos em uma pasta.
218
+
219
+ Args:
220
+ pasta: Caminho da pasta a ser listada.
221
+ extensao: Filtrar por extensão (ex: ".csv", ".json").
222
+ Se None, lista todos os arquivos.
223
+ recursivo: Se True, inclui arquivos em subpastas.
224
+
225
+ Returns:
226
+ Lista de objetos Path para cada arquivo encontrado.
227
+
228
+ Raises:
229
+ DiretorioError: Se a pasta não existir ou não for acessível.
230
+
231
+ Examples:
232
+ Listar todos os CSVs:
233
+
234
+ >>> arquivos = listar_arquivos("/tmp/dados", extensao=".csv")
235
+ >>> for arq in arquivos:
236
+ ... print(arq.name)
237
+
238
+ Listar recursivamente:
239
+
240
+ >>> arquivos = listar_arquivos("/tmp/dados", recursivo=True)
241
+ """
242
+ try:
243
+ pasta_path = Path(pasta)
244
+
245
+ if not pasta_path.exists():
246
+ raise DiretorioError(
247
+ f"Pasta '{pasta}' não existe",
248
+ details={"pasta": pasta}
249
+ )
250
+
251
+ if not pasta_path.is_dir():
252
+ raise DiretorioError(
253
+ f"'{pasta}' não é um diretório",
254
+ details={"pasta": pasta}
255
+ )
256
+
257
+ if recursivo:
258
+ pattern = "**/*" if extensao is None else f"**/*{extensao}"
259
+ arquivos = [p for p in pasta_path.glob(pattern) if p.is_file()]
260
+ else:
261
+ pattern = "*" if extensao is None else f"*{extensao}"
262
+ arquivos = [p for p in pasta_path.glob(pattern) if p.is_file()]
263
+
264
+ return sorted(arquivos)
265
+
266
+ except PermissionError as e:
267
+ raise DiretorioError(
268
+ f"Sem permissão para acessar a pasta '{pasta}'",
269
+ details={"pasta": pasta, "erro": str(e)}
270
+ ) from e