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.
@@ -1,88 +1,147 @@
1
- """Módulo responsável por enviar e-mails com ou sem anexo via SMTP."""
1
+ """Módulo responsável por enviar e-mails via SMTP.
2
+
3
+ Fornece funções para envio de emails com suporte a anexos,
4
+ múltiplos destinatários e configuração via variáveis de ambiente.
5
+
6
+ Examples:
7
+ Envio simples:
8
+
9
+ >>> from nia_etl_utils import enviar_email_smtp
10
+ >>> enviar_email_smtp(
11
+ ... corpo_do_email="Pipeline concluído com sucesso",
12
+ ... assunto="[PROD] ETL Finalizado"
13
+ ... )
14
+
15
+ Envio com configuração explícita:
16
+
17
+ >>> from nia_etl_utils import enviar_email, SmtpConfig
18
+ >>> config = SmtpConfig(
19
+ ... servidor="smtp.empresa.com",
20
+ ... porta=587,
21
+ ... remetente="sistema@empresa.com",
22
+ ... destinatarios_padrao=["admin@empresa.com"]
23
+ ... )
24
+ >>> resultado = enviar_email(
25
+ ... config=config,
26
+ ... corpo="Relatório em anexo",
27
+ ... assunto="Relatório Mensal",
28
+ ... anexo="/tmp/relatorio.pdf"
29
+ ... )
30
+ """
31
+
2
32
  import os
3
33
  import smtplib
4
- import sys
5
34
  from email import encoders
6
35
  from email.mime.base import MIMEBase
7
36
  from email.mime.multipart import MIMEMultipart
8
37
  from email.mime.text import MIMEText
38
+ from pathlib import Path
39
+
9
40
  from loguru import logger
10
41
 
42
+ from .config import SmtpConfig
11
43
  from .env_config import obter_variavel_env
44
+ from .exceptions import DestinatarioError, LeituraArquivoError, SmtpError
45
+ from .results import ResultadoEmail
12
46
 
13
47
 
14
48
  def obter_destinatarios_padrao() -> list[str]:
15
- """Obtém lista de destinatários da variável de ambiente EMAIL_DESTINATARIOS.
49
+ """Obtém lista de destinatários da variável de ambiente.
50
+
51
+ Busca a variável EMAIL_DESTINATARIOS e faz parse dos emails
52
+ separados por vírgula.
16
53
 
17
54
  Returns:
18
- Lista de emails extraída da env var.
55
+ Lista de endereços de email.
19
56
 
20
57
  Raises:
21
- SystemExit: Se EMAIL_DESTINATARIOS estiver vazia ou contiver apenas valores inválidos.
58
+ DestinatarioError: Se EMAIL_DESTINATARIOS estiver vazia
59
+ ou contiver apenas valores inválidos.
60
+
61
+ Examples:
62
+ >>> # Com EMAIL_DESTINATARIOS="admin@x.com,dev@x.com"
63
+ >>> destinatarios = obter_destinatarios_padrao()
64
+ >>> print(destinatarios)
65
+ ['admin@x.com', 'dev@x.com']
22
66
  """
23
67
  destinatarios_str = obter_variavel_env("EMAIL_DESTINATARIOS").strip()
24
- destinatarios = [email.strip() for email in destinatarios_str.split(',') if email.strip()]
68
+ destinatarios = [
69
+ email.strip()
70
+ for email in destinatarios_str.split(',')
71
+ if email.strip()
72
+ ]
25
73
 
26
74
  if not destinatarios:
27
- logger.error(
75
+ raise DestinatarioError(
28
76
  "EMAIL_DESTINATARIOS está vazia ou contém apenas valores inválidos. "
29
77
  "Configure a variável com destinatários separados por vírgula."
30
78
  )
31
- sys.exit(1)
32
79
 
33
80
  return destinatarios
34
81
 
35
82
 
36
- def enviar_email_smtp(
37
- corpo_do_email: str,
83
+ def enviar_email(
84
+ config: SmtpConfig,
85
+ corpo: str,
38
86
  assunto: str,
39
87
  destinatarios: list[str] | None = None,
40
88
  anexo: str | None = None
41
- ) -> None:
42
- """Envia um e-mail com ou sem anexo via SMTP.
89
+ ) -> ResultadoEmail:
90
+ """Envia email via SMTP usando configuração explícita.
43
91
 
44
92
  Args:
45
- corpo_do_email: Texto que será enviado no corpo do e-mail.
93
+ config: Configuração do servidor SMTP.
94
+ corpo: Texto do corpo do email.
46
95
  assunto: Assunto da mensagem.
47
- destinatarios: Lista de endereços de e-mail. Se None, usa EMAIL_DESTINATARIOS da env var.
48
- anexo: Caminho para arquivo anexo. Defaults to None.
96
+ destinatarios: Lista de destinatários. Se None, usa
97
+ config.destinatarios_padrao.
98
+ anexo: Caminho para arquivo anexo (opcional).
99
+
100
+ Returns:
101
+ ResultadoEmail com status do envio.
49
102
 
50
103
  Raises:
51
- SystemExit: Se destinatários não forem fornecidos e EMAIL_DESTINATARIOS não existir,
52
- ou se ocorrer qualquer erro durante o envio do email.
104
+ DestinatarioError: Se nenhum destinatário for fornecido.
105
+ LeituraArquivoError: Se arquivo de anexo não existir.
106
+ SmtpError: Se houver erro na comunicação com servidor SMTP.
53
107
 
54
108
  Examples:
55
- >>> # Uso padrão (destinatários da env var)
56
- >>> enviar_email_smtp(
57
- ... corpo_do_email="Pipeline concluído",
58
- ... assunto="[PROD] ETL Finalizado"
59
- ... )
60
-
61
- >>> # Com destinatários específicos
62
- >>> enviar_email_smtp(
63
- ... corpo_do_email="Relatório executivo",
64
- ... assunto="Relatório Mensal",
65
- ... destinatarios=["diretor@mprj.mp.br"],
109
+ >>> config = SmtpConfig.from_env()
110
+ >>> resultado = enviar_email(
111
+ ... config=config,
112
+ ... corpo="Relatório em anexo",
113
+ ... assunto="Relatório Diário",
66
114
  ... anexo="/tmp/relatorio.pdf"
67
115
  ... )
116
+ >>> if resultado.sucesso:
117
+ ... print("Email enviado!")
68
118
  """
69
- if destinatarios is None:
70
- destinatarios = obter_destinatarios_padrao()
119
+ # Define destinatários
120
+ dest = destinatarios or config.destinatarios_padrao
71
121
 
72
- if not destinatarios:
73
- logger.error("Nenhum destinatário fornecido. E-mail não pode ser enviado.")
74
- sys.exit(1)
122
+ if not dest:
123
+ raise DestinatarioError("Nenhum destinatário fornecido para envio do email")
124
+
125
+ # Valida anexo se fornecido
126
+ if anexo and not Path(anexo).exists():
127
+ raise LeituraArquivoError(
128
+ f"Arquivo de anexo não encontrado: {anexo}",
129
+ details={"caminho": anexo}
130
+ )
75
131
 
76
132
  try:
133
+ # Monta mensagem
77
134
  email_msg = MIMEMultipart()
78
- email_msg['From'] = obter_variavel_env('MAIL_SENDER')
79
- email_msg['To'] = ", ".join(destinatarios)
80
- email_msg['Cc'] = 'gadg.etl@mprj.mp.br'
135
+ email_msg['From'] = config.remetente
136
+ email_msg['To'] = ", ".join(dest)
137
+ if config.cc:
138
+ email_msg['Cc'] = config.cc
81
139
  email_msg['Subject'] = assunto
82
140
 
83
- corpo_da_mensagem = MIMEText(corpo_do_email, 'plain')
141
+ corpo_da_mensagem = MIMEText(corpo, 'plain')
84
142
  email_msg.attach(corpo_da_mensagem)
85
143
 
144
+ # Anexa arquivo se fornecido
86
145
  if anexo:
87
146
  attachment = MIMEBase('application', 'octet-stream')
88
147
  with open(anexo, 'rb') as attachment_file:
@@ -94,33 +153,108 @@ def enviar_email_smtp(
94
153
  )
95
154
  email_msg.attach(attachment)
96
155
 
97
- logger.info("Iniciando conexão com o servidor SMTP...")
98
- with smtplib.SMTP(
99
- obter_variavel_env('MAIL_SMTP_SERVER'),
100
- int(obter_variavel_env('MAIL_SMTP_PORT'))
101
- ) as server:
102
- server.sendmail(
103
- obter_variavel_env('MAIL_SENDER'),
104
- destinatarios,
105
- email_msg.as_string()
106
- )
107
- logger.info(f"E-mail enviado com sucesso para {destinatarios}")
108
-
109
- except smtplib.SMTPRecipientsRefused as error:
110
- logger.error(f"Destinatários recusaram o e-mail: {error}")
111
- sys.exit(1)
112
- except smtplib.SMTPDataError as error:
113
- logger.error(f"Erro durante a transferência de dados: {error}")
114
- sys.exit(1)
115
- except smtplib.SMTPException as error:
116
- logger.error(f"Erro ao enviar o e-mail: {error}")
117
- sys.exit(1)
118
- except ConnectionError as error:
119
- logger.error(f"Erro de conexão com servidor SMTP: {error}")
120
- sys.exit(1)
121
- except FileNotFoundError as error:
122
- logger.error(f"Arquivo de anexo não encontrado: {error}")
123
- sys.exit(1)
124
- except Exception as error:
125
- logger.error(f"Erro inesperado ao enviar e-mail: {error}")
126
- sys.exit(1)
156
+ # Envia
157
+ logger.info(f"Conectando ao servidor SMTP {config.servidor}:{config.porta}...")
158
+
159
+ with smtplib.SMTP(config.servidor, config.porta) as server:
160
+ server.sendmail(config.remetente, dest, email_msg.as_string())
161
+ logger.success(f"Email enviado com sucesso para {dest}")
162
+
163
+ return ResultadoEmail(
164
+ sucesso=True,
165
+ destinatarios=dest,
166
+ assunto=assunto,
167
+ anexo=anexo
168
+ )
169
+
170
+ except smtplib.SMTPRecipientsRefused as e:
171
+ raise SmtpError(
172
+ "Destinatários recusaram o email",
173
+ details={"destinatarios": dest, "erro": str(e)}
174
+ ) from e
175
+ except smtplib.SMTPDataError as e:
176
+ raise SmtpError(
177
+ "Erro durante transferência de dados SMTP",
178
+ details={"erro": str(e)}
179
+ ) from e
180
+ except smtplib.SMTPException as e:
181
+ raise SmtpError(
182
+ "Erro SMTP ao enviar email",
183
+ details={"servidor": config.servidor, "erro": str(e)}
184
+ ) from e
185
+ except ConnectionError as e:
186
+ raise SmtpError(
187
+ f"Erro de conexão com servidor SMTP {config.servidor}",
188
+ details={"servidor": config.servidor, "porta": config.porta, "erro": str(e)}
189
+ ) from e
190
+
191
+
192
+ def enviar_email_smtp(
193
+ corpo_do_email: str,
194
+ assunto: str,
195
+ destinatarios: list[str] | None = None,
196
+ anexo: str | None = None
197
+ ) -> ResultadoEmail:
198
+ """Envia email via SMTP usando variáveis de ambiente.
199
+
200
+ Função de conveniência que carrega configuração de variáveis
201
+ de ambiente automaticamente.
202
+
203
+ Variáveis utilizadas:
204
+ - MAIL_SMTP_SERVER: Endereço do servidor SMTP
205
+ - MAIL_SMTP_PORT: Porta do servidor
206
+ - MAIL_SENDER: Endereço do remetente
207
+ - EMAIL_DESTINATARIOS: Destinatários padrão (separados por vírgula)
208
+ - MAIL_CC: Endereço para cópia (opcional)
209
+
210
+ Args:
211
+ corpo_do_email: Texto que será enviado no corpo do email.
212
+ assunto: Assunto da mensagem.
213
+ destinatarios: Lista de endereços de email. Se None, usa
214
+ EMAIL_DESTINATARIOS da variável de ambiente.
215
+ anexo: Caminho para arquivo anexo (opcional).
216
+
217
+ Returns:
218
+ ResultadoEmail com status do envio.
219
+
220
+ Raises:
221
+ DestinatarioError: Se nenhum destinatário for fornecido e
222
+ EMAIL_DESTINATARIOS não existir.
223
+ LeituraArquivoError: Se arquivo de anexo não existir.
224
+ SmtpError: Se houver erro na comunicação com servidor SMTP.
225
+ ConfiguracaoError: Se variáveis de ambiente estiverem ausentes.
226
+
227
+ Examples:
228
+ Uso padrão (destinatários da variável de ambiente):
229
+
230
+ >>> enviar_email_smtp(
231
+ ... corpo_do_email="Pipeline concluído",
232
+ ... assunto="[PROD] ETL Finalizado"
233
+ ... )
234
+
235
+ Com destinatários específicos:
236
+
237
+ >>> enviar_email_smtp(
238
+ ... corpo_do_email="Relatório executivo",
239
+ ... assunto="Relatório Mensal",
240
+ ... destinatarios=["diretor@mprj.mp.br"],
241
+ ... anexo="/tmp/relatorio.pdf"
242
+ ... )
243
+
244
+ Com tratamento de erro:
245
+
246
+ >>> from nia_etl_utils.exceptions import SmtpError
247
+ >>> try:
248
+ ... enviar_email_smtp("Teste", "Assunto Teste")
249
+ ... except SmtpError as e:
250
+ ... logger.error(f"Falha no envio: {e}")
251
+ """
252
+ config = SmtpConfig.from_env()
253
+
254
+ return enviar_email(
255
+ config=config,
256
+ corpo=corpo_do_email,
257
+ assunto=assunto,
258
+ destinatarios=destinatarios,
259
+ anexo=anexo
260
+ )
@@ -1,43 +1,165 @@
1
- """Módulo utilitário para buscar variáveis de ambiente com suporte a valor padrão e log de erro."""
1
+ """Módulo utilitário para gerenciamento de variáveis de ambiente.
2
+
3
+ Fornece funções para buscar variáveis de ambiente com suporte a
4
+ valores padrão, validação e logging apropriado.
5
+
6
+ Examples:
7
+ Variável obrigatória:
8
+
9
+ >>> from nia_etl_utils import obter_variavel_env
10
+ >>> db_host = obter_variavel_env('DB_HOST') # levanta exceção se não existir
11
+
12
+ Variável opcional com fallback:
13
+
14
+ >>> porta = obter_variavel_env('DB_PORT', default='5432')
15
+
16
+ Tratamento de erro:
17
+
18
+ >>> from nia_etl_utils.exceptions import VariavelAmbienteError
19
+ >>> try:
20
+ ... valor = obter_variavel_env('VARIAVEL_INEXISTENTE')
21
+ ... except VariavelAmbienteError as e:
22
+ ... print(f"Variável não encontrada: {e.nome_variavel}")
23
+ """
24
+
2
25
  import os
3
- import sys
26
+
4
27
  from dotenv import load_dotenv
5
28
  from loguru import logger
6
29
 
30
+ from .exceptions import VariavelAmbienteError
31
+
32
+ # Carrega variáveis do arquivo .env se existir
7
33
  load_dotenv()
8
34
 
9
35
 
10
- def obter_variavel_env(nome_env: str, default=None):
11
- """Retorna o valor de uma variável de ambiente, com fallback opcional.
36
+ def obter_variavel_env(nome_env: str, default: str | None = None) -> str:
37
+ """Retorna o valor de uma variável de ambiente.
12
38
 
13
- Se a variável não existir e nenhum valor padrão for fornecido, o processo
14
- é encerrado com código de saída 1 (falha), garantindo que pipelines no
15
- Airflow detectem o erro corretamente.
39
+ Busca uma variável de ambiente pelo nome. Se a variável não existir
40
+ e nenhum valor padrão for fornecido, levanta VariavelAmbienteError.
16
41
 
17
42
  Args:
18
43
  nome_env: Nome da variável de ambiente a ser buscada.
19
- default: Valor a ser retornado caso a variável não esteja definida. Defaults to None.
44
+ default: Valor a ser retornado caso a variável não esteja definida.
45
+ Se None (padrão), a variável é considerada obrigatória.
20
46
 
21
47
  Returns:
22
- str: Valor da variável de ambiente ou valor padrão.
48
+ Valor da variável de ambiente ou valor padrão.
23
49
 
24
50
  Raises:
25
- SystemExit: Se a variável não for encontrada e nenhum valor padrão for fornecido.
51
+ VariavelAmbienteError: Se a variável não for encontrada e
52
+ nenhum valor padrão for fornecido.
26
53
 
27
54
  Examples:
28
- >>> # Variável obrigatória (falha se não existir)
29
- >>> db_host = obter_variavel_env('DB_HOST')
55
+ Variável obrigatória (falha se não existir):
56
+
57
+ >>> db_host = obter_variavel_env('DB_POSTGRESQL_HOST')
58
+ >>> print(f"Conectando em {db_host}")
59
+
60
+ Variável opcional com fallback:
30
61
 
31
- >>> # Variável opcional com fallback
32
62
  >>> porta = obter_variavel_env('DB_PORT', default='5432')
63
+ >>> timeout = obter_variavel_env('DB_TIMEOUT', default='30')
64
+
65
+ Tratando ausência de variável:
66
+
67
+ >>> from nia_etl_utils.exceptions import VariavelAmbienteError
68
+ >>> try:
69
+ ... api_key = obter_variavel_env('API_KEY_SECRETA')
70
+ ... except VariavelAmbienteError as e:
71
+ ... logger.error(f"Configure a variável: {e.nome_variavel}")
72
+ ... raise
33
73
  """
34
74
  value = os.getenv(nome_env, default)
35
75
 
36
76
  if value is None:
37
77
  logger.error(
38
- f"Variável de ambiente '{nome_env}' não encontrada e nenhum valor padrão foi fornecido. "
78
+ f"Variável de ambiente '{nome_env}' não encontrada "
79
+ f"e nenhum valor padrão foi fornecido. "
39
80
  f"Configure esta variável antes de executar o script."
40
81
  )
41
- sys.exit(1)
82
+ raise VariavelAmbienteError(nome_env)
42
83
 
43
84
  return value
85
+
86
+
87
+ def obter_variavel_env_int(nome_env: str, default: int | None = None) -> int:
88
+ """Retorna o valor de uma variável de ambiente como inteiro.
89
+
90
+ Conveniência para variáveis numéricas como portas e timeouts.
91
+
92
+ Args:
93
+ nome_env: Nome da variável de ambiente.
94
+ default: Valor inteiro padrão se variável não existir.
95
+
96
+ Returns:
97
+ Valor da variável convertido para inteiro.
98
+
99
+ Raises:
100
+ VariavelAmbienteError: Se a variável não existir e não houver default.
101
+ ValueError: Se o valor não puder ser convertido para inteiro.
102
+
103
+ Examples:
104
+ >>> porta = obter_variavel_env_int('DB_PORT', default=5432)
105
+ >>> timeout = obter_variavel_env_int('TIMEOUT_SEGUNDOS', default=30)
106
+ """
107
+ default_str = str(default) if default is not None else None
108
+ valor = obter_variavel_env(nome_env, default=default_str)
109
+ return int(valor)
110
+
111
+
112
+ def obter_variavel_env_bool(nome_env: str, default: bool = False) -> bool:
113
+ """Retorna o valor de uma variável de ambiente como booleano.
114
+
115
+ Valores considerados True: 'true', '1', 'yes', 'on' (case insensitive).
116
+ Qualquer outro valor é considerado False.
117
+
118
+ Args:
119
+ nome_env: Nome da variável de ambiente.
120
+ default: Valor booleano padrão se variável não existir.
121
+
122
+ Returns:
123
+ Valor da variável interpretado como booleano.
124
+
125
+ Examples:
126
+ >>> debug = obter_variavel_env_bool('DEBUG_MODE', default=False)
127
+ >>> verbose = obter_variavel_env_bool('VERBOSE_LOGGING')
128
+ """
129
+ default_str = str(default).lower()
130
+ valor = obter_variavel_env(nome_env, default=default_str)
131
+ return valor.lower() in ('true', '1', 'yes', 'on')
132
+
133
+
134
+ def obter_variavel_env_lista(
135
+ nome_env: str,
136
+ separador: str = ',',
137
+ default: list[str] | None = None
138
+ ) -> list[str]:
139
+ """Retorna o valor de uma variável de ambiente como lista.
140
+
141
+ Útil para variáveis que contêm múltiplos valores separados
142
+ por um delimitador.
143
+
144
+ Args:
145
+ nome_env: Nome da variável de ambiente.
146
+ separador: Caractere separador dos valores. Padrão: vírgula.
147
+ default: Lista padrão se variável não existir.
148
+
149
+ Returns:
150
+ Lista de strings extraída da variável.
151
+
152
+ Raises:
153
+ VariavelAmbienteError: Se a variável não existir e não houver default.
154
+
155
+ Examples:
156
+ >>> # EMAIL_DESTINATARIOS="admin@x.com,dev@x.com"
157
+ >>> emails = obter_variavel_env_lista('EMAIL_DESTINATARIOS')
158
+ >>> # ['admin@x.com', 'dev@x.com']
159
+
160
+ >>> # HOSTS="host1;host2;host3"
161
+ >>> hosts = obter_variavel_env_lista('HOSTS', separador=';')
162
+ """
163
+ default_str = separador.join(default) if default is not None else None
164
+ valor = obter_variavel_env(nome_env, default=default_str)
165
+ return [item.strip() for item in valor.split(separador) if item.strip()]