nia-etl-utils 0.1.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 +126 -0
- nia_etl_utils/database.py +246 -0
- nia_etl_utils/email_smtp.py +126 -0
- nia_etl_utils/env_config.py +43 -0
- nia_etl_utils/limpeza_pastas.py +137 -0
- nia_etl_utils/logger_config.py +146 -0
- nia_etl_utils/processa_csv.py +209 -0
- nia_etl_utils/processa_csv_paralelo.py +151 -0
- nia_etl_utils-0.1.0.dist-info/METADATA +594 -0
- nia_etl_utils-0.1.0.dist-info/RECORD +12 -0
- nia_etl_utils-0.1.0.dist-info/WHEEL +5 -0
- nia_etl_utils-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Utilitários compartilhados para pipelines ETL do NIA/MPRJ.
|
|
2
|
+
|
|
3
|
+
Este pacote fornece funções reutilizáveis para:
|
|
4
|
+
- Configuração de ambiente (env_config)
|
|
5
|
+
- Envio de emails via SMTP (email_smtp)
|
|
6
|
+
- Conexões com bancos de dados Oracle e PostgreSQL (database)
|
|
7
|
+
- Configuração de logging padronizado (logger_config)
|
|
8
|
+
- Processamento e exportação de CSV (processa_csv)
|
|
9
|
+
- Manipulação de arquivos e diretórios (limpeza_pastas)
|
|
10
|
+
|
|
11
|
+
Exemplo de uso:
|
|
12
|
+
from nia_etl_utils import obter_variavel_env, configurar_logger_padrao_nia
|
|
13
|
+
from nia_etl_utils import conectar_postgresql_nia, exportar_para_csv
|
|
14
|
+
|
|
15
|
+
# Configuração
|
|
16
|
+
configurar_logger_padrao_nia("meu_pipeline")
|
|
17
|
+
db_host = obter_variavel_env('DB_POSTGRESQL_HOST')
|
|
18
|
+
|
|
19
|
+
# Conexão e processamento
|
|
20
|
+
cur, conn = conectar_postgresql_nia()
|
|
21
|
+
# ... processar dados ...
|
|
22
|
+
exportar_para_csv(df, "resultado", "2025_01_19", "/dados")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
__author__ = "Nícolas Galdino Esmael"
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# IMPORTS PRINCIPAIS - Funções mais usadas exportadas diretamente
|
|
30
|
+
# ============================================================================
|
|
31
|
+
|
|
32
|
+
# Configuração de ambiente
|
|
33
|
+
from .env_config import obter_variavel_env
|
|
34
|
+
|
|
35
|
+
# Email
|
|
36
|
+
from .email_smtp import enviar_email_smtp, obter_destinatarios_padrao
|
|
37
|
+
|
|
38
|
+
# Database - PostgreSQL
|
|
39
|
+
from .database import (
|
|
40
|
+
conectar_postgresql,
|
|
41
|
+
conectar_postgresql_nia,
|
|
42
|
+
conectar_postgresql_opengeo,
|
|
43
|
+
obter_engine_postgresql,
|
|
44
|
+
obter_engine_postgresql_nia,
|
|
45
|
+
obter_engine_postgresql_opengeo,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Database - Oracle
|
|
49
|
+
from .database import (
|
|
50
|
+
conectar_oracle,
|
|
51
|
+
conectar_oracle_ouvidorias,
|
|
52
|
+
fechar_conexao,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Logging
|
|
56
|
+
from .logger_config import (
|
|
57
|
+
configurar_logger,
|
|
58
|
+
configurar_logger_padrao_nia,
|
|
59
|
+
remover_handlers_existentes,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Processamento CSV
|
|
63
|
+
from .processa_csv import (
|
|
64
|
+
exportar_para_csv,
|
|
65
|
+
extrair_e_exportar_csv,
|
|
66
|
+
exportar_multiplos_csv,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Processamento CSV Paralelo
|
|
70
|
+
from .processa_csv_paralelo import (
|
|
71
|
+
processar_csv_paralelo,
|
|
72
|
+
calcular_chunksize,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Manipulação de arquivos
|
|
76
|
+
from .limpeza_pastas import (
|
|
77
|
+
limpar_pasta,
|
|
78
|
+
remover_pasta_recursivamente,
|
|
79
|
+
criar_pasta_se_nao_existir,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# Metadata
|
|
85
|
+
"__version__",
|
|
86
|
+
"__author__",
|
|
87
|
+
|
|
88
|
+
# Env config
|
|
89
|
+
"obter_variavel_env",
|
|
90
|
+
|
|
91
|
+
# Email
|
|
92
|
+
"enviar_email_smtp",
|
|
93
|
+
"obter_destinatarios_padrao",
|
|
94
|
+
|
|
95
|
+
# Database - PostgreSQL
|
|
96
|
+
"conectar_postgresql",
|
|
97
|
+
"conectar_postgresql_nia",
|
|
98
|
+
"conectar_postgresql_opengeo",
|
|
99
|
+
"obter_engine_postgresql",
|
|
100
|
+
"obter_engine_postgresql_nia",
|
|
101
|
+
"obter_engine_postgresql_opengeo",
|
|
102
|
+
|
|
103
|
+
# Database - Oracle
|
|
104
|
+
"conectar_oracle",
|
|
105
|
+
"conectar_oracle_ouvidorias",
|
|
106
|
+
"fechar_conexao",
|
|
107
|
+
|
|
108
|
+
# Logging
|
|
109
|
+
"configurar_logger",
|
|
110
|
+
"configurar_logger_padrao_nia",
|
|
111
|
+
"remover_handlers_existentes",
|
|
112
|
+
|
|
113
|
+
# CSV
|
|
114
|
+
"exportar_para_csv",
|
|
115
|
+
"extrair_e_exportar_csv",
|
|
116
|
+
"exportar_multiplos_csv",
|
|
117
|
+
|
|
118
|
+
# CSV Paralelo
|
|
119
|
+
"processar_csv_paralelo",
|
|
120
|
+
"calcular_chunksize",
|
|
121
|
+
|
|
122
|
+
# Arquivos
|
|
123
|
+
"limpar_pasta",
|
|
124
|
+
"remover_pasta_recursivamente",
|
|
125
|
+
"criar_pasta_se_nao_existir",
|
|
126
|
+
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Módulo de conexão com bancos de dados PostgreSQL e Oracle.
|
|
2
|
+
|
|
3
|
+
Fornece funções genéricas e wrappers de conveniência para estabelecer conexões
|
|
4
|
+
com diferentes bancos de dados usando credenciais de variáveis de ambiente.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import cx_Oracle
|
|
8
|
+
import psycopg2
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from psycopg2 import Error, OperationalError
|
|
11
|
+
from sqlalchemy import create_engine
|
|
12
|
+
from sqlalchemy.engine import Engine
|
|
13
|
+
|
|
14
|
+
from .env_config import obter_variavel_env
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def conectar_postgresql(sufixo_env: str = "") -> tuple:
|
|
18
|
+
"""Estabelece conexão genérica com banco de dados PostgreSQL usando psycopg2.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
sufixo_env: Sufixo das variáveis de ambiente para conexão.
|
|
22
|
+
Vazio ("") para banco padrão (DB_POSTGRESQL_*).
|
|
23
|
+
"_OPENGEO" para OpenGeo (DB_POSTGRESQL_HOST_OPENGEO, etc).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
tuple: (cursor, connection) — cursor e objeto de conexão ativa.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
SystemExit: Se houver erro ao estabelecer a conexão.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
>>> # Conecta usando variáveis DB_POSTGRESQL_HOST, DB_POSTGRESQL_PORT, etc
|
|
33
|
+
>>> cur, conn = conectar_postgresql()
|
|
34
|
+
>>> cur.execute("SELECT * FROM tabela")
|
|
35
|
+
>>> resultados = cur.fetchall()
|
|
36
|
+
>>> cur.close()
|
|
37
|
+
>>> conn.close()
|
|
38
|
+
|
|
39
|
+
>>> # Conecta em OpenGeo usando DB_POSTGRESQL_HOST_OPENGEO, etc
|
|
40
|
+
>>> cur, conn = conectar_postgresql("_OPENGEO")
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
host = obter_variavel_env(f"DB_POSTGRESQL_HOST{sufixo_env}")
|
|
44
|
+
port = obter_variavel_env(f"DB_POSTGRESQL_PORT{sufixo_env}")
|
|
45
|
+
database = obter_variavel_env(f"DB_POSTGRESQL_DATABASE{sufixo_env}")
|
|
46
|
+
user = obter_variavel_env(f"DB_POSTGRESQL_USER{sufixo_env}")
|
|
47
|
+
password = obter_variavel_env(f"DB_POSTGRESQL_PASSWORD{sufixo_env}")
|
|
48
|
+
|
|
49
|
+
logger.info(f"Iniciando conexão PostgreSQL em {host}:{port}, database '{database}'...")
|
|
50
|
+
|
|
51
|
+
connection = psycopg2.connect(
|
|
52
|
+
host=host,
|
|
53
|
+
port=port,
|
|
54
|
+
database=database,
|
|
55
|
+
user=user,
|
|
56
|
+
password=password
|
|
57
|
+
)
|
|
58
|
+
cursor = connection.cursor()
|
|
59
|
+
|
|
60
|
+
logger.success(f"Conexão PostgreSQL estabelecida: '{database}' em '{host}:{port}'")
|
|
61
|
+
return cursor, connection
|
|
62
|
+
|
|
63
|
+
except OperationalError as error:
|
|
64
|
+
logger.error(f"Erro operacional PostgreSQL (sufixo: '{sufixo_env}'): {error}")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
except Error as error:
|
|
67
|
+
logger.error(f"Erro PostgreSQL (sufixo: '{sufixo_env}'): {error}")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
except Exception as error:
|
|
70
|
+
logger.error(f"Erro inesperado PostgreSQL (sufixo: '{sufixo_env}'): {error}")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def obter_engine_postgresql(sufixo_env: str = "") -> Engine:
|
|
75
|
+
"""Cria uma engine SQLAlchemy genérica para PostgreSQL.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
sufixo_env: Sufixo das variáveis de ambiente para conexão.
|
|
79
|
+
Vazio ("") para banco padrão, "_OPENGEO" para OpenGeo.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Engine configurada para PostgreSQL.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
SystemExit: Se houver erro na criação da conexão.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
>>> import pandas as pd
|
|
89
|
+
>>> engine = obter_engine_postgresql()
|
|
90
|
+
>>> df = pd.read_sql("SELECT * FROM tabela", engine)
|
|
91
|
+
|
|
92
|
+
>>> # Engine para OpenGeo
|
|
93
|
+
>>> engine = obter_engine_postgresql("_OPENGEO")
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
host = obter_variavel_env(f"DB_POSTGRESQL_HOST{sufixo_env}")
|
|
97
|
+
port = obter_variavel_env(f"DB_POSTGRESQL_PORT{sufixo_env}")
|
|
98
|
+
database = obter_variavel_env(f"DB_POSTGRESQL_DATABASE{sufixo_env}")
|
|
99
|
+
user = obter_variavel_env(f"DB_POSTGRESQL_USER{sufixo_env}")
|
|
100
|
+
password = obter_variavel_env(f"DB_POSTGRESQL_PASSWORD{sufixo_env}")
|
|
101
|
+
|
|
102
|
+
connection_string = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}"
|
|
103
|
+
engine = create_engine(connection_string)
|
|
104
|
+
|
|
105
|
+
# Testa a conexão
|
|
106
|
+
with engine.connect() as conn:
|
|
107
|
+
conn.execute("SELECT 1") # type: ignore
|
|
108
|
+
|
|
109
|
+
logger.success(f"Engine PostgreSQL criada: '{database}' em '{host}:{port}'")
|
|
110
|
+
return engine
|
|
111
|
+
|
|
112
|
+
except OperationalError as error:
|
|
113
|
+
logger.error(f"Erro operacional ao criar engine PostgreSQL (sufixo: '{sufixo_env}'): {error}")
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
except Exception as error:
|
|
116
|
+
logger.error(f"Erro inesperado ao criar engine PostgreSQL (sufixo: '{sufixo_env}'): {error}")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def conectar_oracle() -> tuple:
|
|
121
|
+
"""Estabelece conexão com banco de dados Oracle.
|
|
122
|
+
|
|
123
|
+
Usa variáveis de ambiente: DB_ORACLE_HOST, DB_ORACLE_PORT, DB_ORACLE_SERVICE_NAME,
|
|
124
|
+
DB_ORACLE_USER, DB_ORACLE_PASSWORD.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
tuple: (cursor, connection) — cursor e objeto de conexão ativa.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
SystemExit: Se houver erro ao estabelecer a conexão.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
>>> cur, conn = conectar_oracle()
|
|
134
|
+
>>> cur.execute("SELECT * FROM tabela WHERE ROWNUM <= 10")
|
|
135
|
+
>>> resultados = cur.fetchall()
|
|
136
|
+
>>> fechar_conexao(cur, conn)
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
host = obter_variavel_env("DB_ORACLE_HOST")
|
|
140
|
+
port = obter_variavel_env("DB_ORACLE_PORT")
|
|
141
|
+
service_name = obter_variavel_env("DB_ORACLE_SERVICE_NAME")
|
|
142
|
+
user = obter_variavel_env("DB_ORACLE_USER")
|
|
143
|
+
password = obter_variavel_env("DB_ORACLE_PASSWORD")
|
|
144
|
+
|
|
145
|
+
logger.info(f"Iniciando conexão Oracle em {host}:{port}, service '{service_name}'...")
|
|
146
|
+
|
|
147
|
+
dsn = cx_Oracle.makedsn(host, port, service_name=service_name)
|
|
148
|
+
connection = cx_Oracle.connect(user=user, password=password, dsn=dsn)
|
|
149
|
+
cursor = connection.cursor()
|
|
150
|
+
|
|
151
|
+
logger.success(f"Conexão Oracle estabelecida: '{service_name}' em '{host}:{port}'")
|
|
152
|
+
return cursor, connection
|
|
153
|
+
|
|
154
|
+
except cx_Oracle.DatabaseError as error:
|
|
155
|
+
logger.error(f"Erro de banco Oracle: {error}")
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
except cx_Oracle.InterfaceError as error:
|
|
158
|
+
logger.error(f"Erro de interface Oracle: {error}")
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
except Exception as error:
|
|
161
|
+
logger.error(f"Erro inesperado Oracle: {error}")
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def fechar_conexao(cursor, connection) -> None:
|
|
166
|
+
"""Encerra cursor e conexão de banco de dados de forma segura.
|
|
167
|
+
|
|
168
|
+
Funciona para conexões PostgreSQL (psycopg2) e Oracle (cx_Oracle).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
cursor: Cursor ativo que será fechado.
|
|
172
|
+
connection: Conexão ativa que será encerrada.
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> cur, conn = conectar_postgresql()
|
|
176
|
+
>>> # ... usar cursor ...
|
|
177
|
+
>>> fechar_conexao(cur, conn)
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
if cursor:
|
|
181
|
+
cursor.close()
|
|
182
|
+
logger.debug("Cursor fechado com sucesso.")
|
|
183
|
+
|
|
184
|
+
if connection:
|
|
185
|
+
connection.close()
|
|
186
|
+
logger.debug("Conexão encerrada com sucesso.")
|
|
187
|
+
|
|
188
|
+
except Exception as error:
|
|
189
|
+
logger.warning(f"Erro ao fechar conexão: {error}")
|
|
190
|
+
# Não usa sys.exit(1) porque fechar conexão é cleanup
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# =============================================================================
|
|
194
|
+
# WRAPPERS DE CONVENIÊNCIA - APIs específicas para bancos conhecidos
|
|
195
|
+
# =============================================================================
|
|
196
|
+
|
|
197
|
+
def conectar_postgresql_nia() -> tuple:
|
|
198
|
+
"""Conecta no PostgreSQL do NIA.
|
|
199
|
+
|
|
200
|
+
Usa variáveis de ambiente: DB_POSTGRESQL_HOST, DB_POSTGRESQL_PORT, etc.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
tuple: (cursor, connection)
|
|
204
|
+
"""
|
|
205
|
+
return conectar_postgresql("")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def obter_engine_postgresql_nia() -> Engine:
|
|
209
|
+
"""Cria engine SQLAlchemy para PostgreSQL do NIA.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Engine configurada para o banco NIA.
|
|
213
|
+
"""
|
|
214
|
+
return obter_engine_postgresql("")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def conectar_postgresql_opengeo() -> tuple:
|
|
218
|
+
"""Conecta no PostgreSQL OpenGeo.
|
|
219
|
+
|
|
220
|
+
Usa variáveis de ambiente: DB_POSTGRESQL_HOST_OPENGEO, DB_POSTGRESQL_PORT_OPENGEO, etc.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
tuple: (cursor, connection)
|
|
224
|
+
"""
|
|
225
|
+
return conectar_postgresql("_OPENGEO")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def obter_engine_postgresql_opengeo() -> Engine:
|
|
229
|
+
"""Cria engine SQLAlchemy para PostgreSQL OpenGeo.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Engine configurada para o banco OpenGeo.
|
|
233
|
+
"""
|
|
234
|
+
return obter_engine_postgresql("_OPENGEO")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def conectar_oracle_ouvidorias() -> tuple:
|
|
238
|
+
"""Conecta no Oracle de Ouvidorias.
|
|
239
|
+
|
|
240
|
+
Alias para conectar_oracle() — mantido para compatibilidade semântica.
|
|
241
|
+
Usa variáveis de ambiente: DB_ORACLE_HOST, DB_ORACLE_PORT, etc.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
tuple: (cursor, connection)
|
|
245
|
+
"""
|
|
246
|
+
return conectar_oracle()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Módulo responsável por enviar e-mails com ou sem anexo via SMTP."""
|
|
2
|
+
import os
|
|
3
|
+
import smtplib
|
|
4
|
+
import sys
|
|
5
|
+
from email import encoders
|
|
6
|
+
from email.mime.base import MIMEBase
|
|
7
|
+
from email.mime.multipart import MIMEMultipart
|
|
8
|
+
from email.mime.text import MIMEText
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from .env_config import obter_variavel_env
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def obter_destinatarios_padrao() -> list[str]:
|
|
15
|
+
"""Obtém lista de destinatários da variável de ambiente EMAIL_DESTINATARIOS.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Lista de emails extraída da env var.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
SystemExit: Se EMAIL_DESTINATARIOS estiver vazia ou contiver apenas valores inválidos.
|
|
22
|
+
"""
|
|
23
|
+
destinatarios_str = obter_variavel_env("EMAIL_DESTINATARIOS").strip()
|
|
24
|
+
destinatarios = [email.strip() for email in destinatarios_str.split(',') if email.strip()]
|
|
25
|
+
|
|
26
|
+
if not destinatarios:
|
|
27
|
+
logger.error(
|
|
28
|
+
"EMAIL_DESTINATARIOS está vazia ou contém apenas valores inválidos. "
|
|
29
|
+
"Configure a variável com destinatários separados por vírgula."
|
|
30
|
+
)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
return destinatarios
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def enviar_email_smtp(
|
|
37
|
+
corpo_do_email: str,
|
|
38
|
+
assunto: str,
|
|
39
|
+
destinatarios: list[str] | None = None,
|
|
40
|
+
anexo: str | None = None
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Envia um e-mail com ou sem anexo via SMTP.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
corpo_do_email: Texto que será enviado no corpo do e-mail.
|
|
46
|
+
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.
|
|
49
|
+
|
|
50
|
+
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.
|
|
53
|
+
|
|
54
|
+
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"],
|
|
66
|
+
... anexo="/tmp/relatorio.pdf"
|
|
67
|
+
... )
|
|
68
|
+
"""
|
|
69
|
+
if destinatarios is None:
|
|
70
|
+
destinatarios = obter_destinatarios_padrao()
|
|
71
|
+
|
|
72
|
+
if not destinatarios:
|
|
73
|
+
logger.error("Nenhum destinatário fornecido. E-mail não pode ser enviado.")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
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'
|
|
81
|
+
email_msg['Subject'] = assunto
|
|
82
|
+
|
|
83
|
+
corpo_da_mensagem = MIMEText(corpo_do_email, 'plain')
|
|
84
|
+
email_msg.attach(corpo_da_mensagem)
|
|
85
|
+
|
|
86
|
+
if anexo:
|
|
87
|
+
attachment = MIMEBase('application', 'octet-stream')
|
|
88
|
+
with open(anexo, 'rb') as attachment_file:
|
|
89
|
+
attachment.set_payload(attachment_file.read())
|
|
90
|
+
encoders.encode_base64(attachment)
|
|
91
|
+
attachment.add_header(
|
|
92
|
+
'Content-Disposition',
|
|
93
|
+
f'attachment; filename={os.path.basename(anexo)}'
|
|
94
|
+
)
|
|
95
|
+
email_msg.attach(attachment)
|
|
96
|
+
|
|
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)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Módulo utilitário para buscar variáveis de ambiente com suporte a valor padrão e log de erro."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
load_dotenv()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def obter_variavel_env(nome_env: str, default=None):
|
|
11
|
+
"""Retorna o valor de uma variável de ambiente, com fallback opcional.
|
|
12
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
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.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
str: Valor da variável de ambiente ou valor padrão.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
SystemExit: Se a variável não for encontrada e nenhum valor padrão for fornecido.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> # Variável obrigatória (falha se não existir)
|
|
29
|
+
>>> db_host = obter_variavel_env('DB_HOST')
|
|
30
|
+
|
|
31
|
+
>>> # Variável opcional com fallback
|
|
32
|
+
>>> porta = obter_variavel_env('DB_PORT', default='5432')
|
|
33
|
+
"""
|
|
34
|
+
value = os.getenv(nome_env, default)
|
|
35
|
+
|
|
36
|
+
if value is None:
|
|
37
|
+
logger.error(
|
|
38
|
+
f"Variável de ambiente '{nome_env}' não encontrada e nenhum valor padrão foi fornecido. "
|
|
39
|
+
f"Configure esta variável antes de executar o script."
|
|
40
|
+
)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
return value
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Funções utilitárias para manipulação de arquivos e diretórios."""
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def limpar_pasta(pasta: str, log: bool = True) -> None:
|
|
9
|
+
"""Remove todos os arquivos de uma pasta, recriando-a se necessário.
|
|
10
|
+
|
|
11
|
+
Se a pasta não existir, ela será criada. Se existir, todos os arquivos
|
|
12
|
+
dentro dela serão removidos (subdiretórios são preservados).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
pasta: Caminho da pasta que será limpa.
|
|
16
|
+
log: Se True, emite logs com Loguru. Defaults to True.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
SystemExit: Se houver erro ao criar ou limpar a pasta.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
>>> from nia_etl_utils.limpeza_pastas import limpar_pasta
|
|
23
|
+
>>> limpar_pasta("/tmp/meu_pipeline")
|
|
24
|
+
>>> # Pasta criada ou limpa com sucesso
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
pasta_path = Path(pasta)
|
|
28
|
+
|
|
29
|
+
if not pasta_path.exists():
|
|
30
|
+
pasta_path.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
if log:
|
|
32
|
+
logger.info(f"Pasta criada: {pasta}")
|
|
33
|
+
else:
|
|
34
|
+
arquivos_removidos = 0
|
|
35
|
+
|
|
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}")
|
|
42
|
+
|
|
43
|
+
if log:
|
|
44
|
+
logger.info(f"Pasta '{pasta}' limpa com sucesso. {arquivos_removidos} arquivo(s) removido(s).")
|
|
45
|
+
|
|
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)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def remover_pasta_recursivamente(pasta: str, log: bool = True) -> None:
|
|
58
|
+
"""Remove uma pasta e todo seu conteúdo (arquivos e subpastas).
|
|
59
|
+
|
|
60
|
+
ATENÇÃO: Esta função remove TUDO dentro da pasta, incluindo subdiretórios.
|
|
61
|
+
Use com cautela.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
pasta: Caminho da pasta que será removida completamente.
|
|
65
|
+
log: Se True, emite logs com Loguru. Defaults to True.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
SystemExit: Se houver erro ao remover a pasta.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
>>> from nia_etl_utils.limpeza_pastas import remover_pasta_recursivamente
|
|
72
|
+
>>> remover_pasta_recursivamente("/tmp/pasta_temporaria")
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
pasta_path = Path(pasta)
|
|
76
|
+
|
|
77
|
+
if not pasta_path.exists():
|
|
78
|
+
if log:
|
|
79
|
+
logger.warning(f"Pasta '{pasta}' não existe. Nada a remover.")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if not pasta_path.is_dir():
|
|
83
|
+
logger.error(f"'{pasta}' não é um diretório.")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
shutil.rmtree(pasta_path)
|
|
87
|
+
|
|
88
|
+
if log:
|
|
89
|
+
logger.info(f"Pasta '{pasta}' removida completamente (incluindo subpastas).")
|
|
90
|
+
|
|
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)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def criar_pasta_se_nao_existir(pasta: str, log: bool = True) -> None:
|
|
103
|
+
"""Cria uma pasta se ela não existir (incluindo pastas pai).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
pasta: Caminho da pasta que será criada.
|
|
107
|
+
log: Se True, emite logs com Loguru. Defaults to True.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
SystemExit: Se houver erro ao criar a pasta.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
>>> from nia_etl_utils.limpeza_pastas import criar_pasta_se_nao_existir
|
|
114
|
+
>>> criar_pasta_se_nao_existir("/tmp/dados/processados/2025")
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
pasta_path = Path(pasta)
|
|
118
|
+
|
|
119
|
+
if pasta_path.exists():
|
|
120
|
+
if log:
|
|
121
|
+
logger.debug(f"Pasta '{pasta}' já existe.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
pasta_path.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
if log:
|
|
127
|
+
logger.info(f"Pasta criada: {pasta}")
|
|
128
|
+
|
|
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)
|