Initial commit: SaveUrl project migration from Drive

This commit is contained in:
Antigravity Migration Agent
2026-05-09 14:20:17 -04:00
commit 9f799c9398
16 changed files with 1439 additions and 0 deletions
+415
View File
@@ -0,0 +1,415 @@
import requests
import os
import pyodbc
import logging
import json
#from dotenv import load_dotenv
from airflow.models import Variable
from ftplib import FTP
from datetime import datetime
from kafka import KafkaProducer
logging.basicConfig(level=logging.INFO)
airflow_logger = logging.getLogger(__name__)
# ==========================================
# CONFIGURACIÓN
# ==========================================
APP_NAME = 'PYTHON_CREATE_URL-FTP_APP'
VERSION = '2.0.0'
PROCEDURE_ID_SUFFIX = '000301900001'
FTP_BASE_URL = 'https://000oqta.rcomhost.com'
FTP_BASE_PATH = '/www/imglm/2026/'
#WEBHOOK_URL = os.getenv('WEBHOOK_URL_NOTIFICATION') # Reemplaza con tu URL de webhook
WEBHOOK_URL = Variable.get("WEBHOOK_URL_NOTIFICATION")
INFORMATIONS = [
['glmpmi', '_02354', 'glm_pmi', '00180000'],
['glmccf', '_02357', 'glm_ccf', '00190000'],
['glmea', '_02084', 'glm_ea', '00110000'],
['glmcp', '_02059', 'glm_cp', '00070000'],
['glmcl', '_02086', 'glm_cl', '00060000'],
['is001', '_02290', 'IS001', '00010000'],
['is003', '_02301', 'IS003', '00050000'],
['is004', '_02310', 'IS004', '00040000'],
['is005', '_02323', 'IS005', '00090000'],
]
# ==========================================
# KAFKA LOGGER
# ==========================================
class KafkaLogger:
def __init__(self, source,bootstrap_servers, use_ssl):
"""Inicializa el KafkaLogger con la configuración necesaria para conectarse a Kafka.
Args:
source (str): Identificador del origen de los logs, útil para filtrar en Kafka.
"""
self.source = source
self.topic = 'executions_logs'
use_ssl = use_ssl
config = {
'bootstrap_servers': bootstrap_servers,
'value_serializer': lambda v: json.dumps(v, default=str).encode('utf-8'),
'acks': 1,
'retries': 3,
}
if use_ssl:
config.update({
'security_protocol': os.getenv('SECURITY_PROTOCOL'),
'sasl_mechanism': os.getenv('SASL_MECHANISM'),
'sasl_plain_username': os.getenv('SASL_PLAIN_USERNAME'),
'sasl_plain_password': os.getenv('SASL_PLAIN_PASSWORD'),
'ssl_cafile': os.getenv('SSL_CAFILE'),
'ssl_check_hostname': os.getenv('SSL_CHECK_HOSTNAME') == 'True',
})
self.producer = KafkaProducer(**config)
def log(self, database, db_id, procedure_name, status, message, executed_by,
target_date, date, extra_info=None):
"""Envía un mensaje de log a Kafka con la información proporcionada.
Args:
database (str): Nombre de la base de datos relacionada con el log.
db_id (str): Identificador único relacionado con la base de datos.
procedure_name (str): Nombre del procedimiento o tarea que se está registrando.
status (str): Estado del procedimiento (e.g., 'STARTING', 'SUCCESS', 'FAILED').
message (str): Mensaje descriptivo del evento que se está registrando.
executed_by (str): Usuario o entidad que ejecutó el procedimiento.
target_date (str): Fecha objetivo para la ejecución del procedimiento.
date (datetime): Fecha y hora en que se registra el evento.
extra_info (any, optional): Información adicional relevante para el log. Puede ser cualquier tipo de dato serializable a JSON.
"""
data = {
"database": database,
"procedure_id": str(db_id) + PROCEDURE_ID_SUFFIX,
"procedure_name": procedure_name,
"procedure_type": "PYTHON_APP",
"status": status,
"message": message,
"executed_by": executed_by,
"source": self.source,
"extra_info": str(extra_info).replace("'", " ") if extra_info else None,
"target_date": str(target_date),
"date": str(date),
}
try:
self.producer.send(self.topic, value=data).get(timeout=5)
except Exception as e:
print(f"[KafkaLogger] Error: {e}")
def close(self):
self.producer.close()
# ==========================================
# SQL
# ==========================================
def read_table_where(db_database, db_password, db_user, db_server, db_table, folder):
"""
Lee los registros de una tabla filtrando por folder.
Args:
db_database (str): Nombre de la base de datos.
db_password (str): Contraseña del usuario.
db_user (str): Usuario de la base de datos.
db_server (str): Servidor de la base de datos.
db_table (str): Nombre de la tabla.
folder (str): Carpeta a filtrar.
Returns:
list: Lista de diccionarios con los registros.
"""
connection = pyodbc.connect(
f'DRIVER={{ODBC Driver 18 for SQL Server}};'
f'SERVER={db_server};'
f'DATABASE={db_database};'
f'UID={db_user};'
f'PWD={{{db_password}}};'
f'TrustServerCertificate=yes;'
f'Encrypt=no;'
)
cursor = connection.cursor()
cursor.execute(f"SELECT * FROM [{db_table}] WHERE Folder LIKE ?", (f'%{folder}%',))
columns = [col[0] for col in cursor.description]
rows = cursor.fetchall()
cursor.close()
connection.close()
return [dict(zip(columns, row)) for row in rows]
def insert_records(db_database, db_password, db_user, db_server, db_table, data):
"""
Inserta registros en una tabla de SQL Server en lotes de 1000.
Args:
db_database (str): Nombre de la base de datos.
db_password (str): Contraseña del usuario.
db_user (str): Usuario de la base de datos.
db_server (str): Servidor de la base de datos.
db_table (str): Nombre de la tabla.
data (list): Lista de diccionarios con los datos a insertar.
Returns:
int: Número de registros insertados.
"""
if not data:
return 0
connection = pyodbc.connect(
f'DRIVER={{ODBC Driver 18 for SQL Server}};'
f'SERVER={db_server};'
f'DATABASE={db_database};'
f'UID={db_user};'
f'PWD={{{db_password}}};'
f'TrustServerCertificate=yes;'
f'Encrypt=no;'
)
cursor = connection.cursor()
columns = [k for k in data[0].keys() if k not in ['ID', 'ExportDate']]
columns_sql = ", ".join([f'[{c}]' for c in columns])
placeholders = ", ".join(["?"] * len(columns))
query = f"INSERT INTO [{db_table}] ({columns_sql}) VALUES ({placeholders})"
values = [tuple(row[c] for c in columns) for row in data]
batch_size = 1000
inserted = 0
for i in range(0, len(values), batch_size):
batch = values[i:i + batch_size]
try:
cursor.executemany(query, batch)
connection.commit()
inserted += len(batch)
airflow_logger.info(f"Batch {i // batch_size + 1} inserted: {len(batch)} records")
#print(f"Lote {i // batch_size + 1} insertado: {len(batch)} registros")
except Exception as e:
connection.rollback()
log(f'FAILED', f'Error insertando lote {i // batch_size + 1}', extra_info=e)
airflow_logger.error(f"Error insertando lote {i // batch_size + 1}: {e}")
#print(f"Error en lote {i // batch_size + 1}: {e}")
cursor.close()
connection.close()
return inserted
# ==========================================
# FTP
# ==========================================
def read_ftp(ftp_server, ftp_user, ftp_password, code_folder, identity_id, year, month):
"""
Lee los archivos de un servidor FTP y devuelve una lista de URLs.
Args:
ftp_server (str): Dirección del servidor FTP.
ftp_user (str): Nombre de usuario para la conexión FTP.
ftp_password (str): Contraseña para la conexión FTP.
code_folder (str): Carpeta que contiene los archivos a leer.
identity_id (str): ID de identidad para filtrar los archivos.
year (str): Año para filtrar los archivos.
month (str): Mes para filtrar los archivos.
Returns:
list: Lista de URLs de los archivos encontrados.
"""
try:
ftp = FTP(ftp_server)
ftp.encoding = 'utf-8'
ftp.sendcmd('OPTS UTF8 ON')
ftp.login(ftp_user, ftp_password)
except Exception as e:
#print(f"Conexión FTP fallida: {e}")
raise
path = FTP_BASE_PATH + code_folder
file_names = ftp.nlst(path)
urls = [FTP_BASE_URL + f.replace('/www', '') for f in file_names]
results = []
for url in urls:
pos_folder = url.find(code_folder)
pos_date = url.find(f'_{year}-{month}')
pos_id = url.find(identity_id)
results.append({
'Folder': url[pos_folder:pos_folder + len(code_folder)],
'Date': url[pos_date + 1:pos_date + 11],
'IDcodigo': url[pos_id:pos_id + 24],
'ObjectCode': url[pos_date + 12:pos_id],
'TaskName': url[pos_folder + len(code_folder) + 1:pos_date],
'URL': url,
})
ftp.quit()
return results
# ==========================================
# MAIN
# ==========================================
def run_save_url(**context):
airflow_logger.info(f"{'='*50}")
airflow_logger.info(f"▶ INICIANDO {APP_NAME} {VERSION}")
airflow_logger.info(f"{'='*50}")
#print(f"{'='*50}")
#print(f"▶ INICIANDO {APP_NAME} {VERSION}")
#print(f"{'='*50}")
#kafka_logger = KafkaLogger(
# source=f'{APP_NAME}_{VERSION}',
# bootstrap_servers=os.getenv('BOOTSTRAP_SERVERS'),
# use_ssl = os.getenv('KAFKA_USE_SSL', 'False') == 'True'
# )
kafka_logger = KafkaLogger(
source=f'{APP_NAME}_{VERSION}',
bootstrap_servers=Variable.get("BOOTSTRAP_SERVERS_DOCKER"),
use_ssl=Variable.get("KAFKA_USE_SSL", default_var="False") == "True"
)
airflow_logger.info(f"✅ KafkaLogger inicializado | SSL: {os.getenv('KAFKA_USE_SSL')}")
#print
#INICIANDO DE VARIABLES DE ENTORNO
#ftp_server = os.getenv('FTP_000oqta_SERVER')
ftp_server = Variable.get("FTP_000oqta_SERVER")
#ftp_user = os.getenv('FTP_000oqta_USER')
ftp_user = Variable.get("FTP_000oqta_USER")
#ftp_password = os.getenv('FTP_000oqta_PASSWORD')
ftp_password = Variable.get("FTP_000oqta_PASSWORD")
#db_user = os.getenv('DB_MSSQ_USER')
db_user = Variable.get("DB_MSSQ_USER")
#db_password = os.getenv('DB_MSSQ_PASSWORD')
db_password = Variable.get("DB_MSSQ_PASSWORD")
#db_server = os.getenv('DB_MSSQ_SERVER')
db_server = Variable.get("DB_MSSQ_SERVER")
db_table = 'URL_FTP'
now = datetime.now()
today = now.strftime('%Y-%m-%d')
year_folder = now.strftime('%Y')
month_folder = now.strftime('%m')
#INICIANDO LOGICA DEL PROGRAMA
for name_folder, identity_id, db_database, database_id in INFORMATIONS:
code_folder = name_folder + month_folder
procedure_name = f'Generacion de URL {db_database}'
def log(status, message, extra_info=None):
kafka_logger.log(
database=db_database, db_id=database_id,
procedure_name=procedure_name, status=status,
message=message, executed_by=db_user,
target_date=today, date=now,
extra_info=extra_info
)
def send_whatsapp(step, error):
message = (
f"🚨 *ALERTA PIPELINE*\n"
f"━━━━━━━━━━━━━━━\n"
f"📦 *BD:* {db_database}\n"
f"⚙️ *Paso:* {step}\n"
f"❌ *Error:* {str(error)[:200]}\n"
f"🕐 *Hora:* {datetime.now()}\n"
f"━━━━━━━━━━━━━━━\n"
f"📌 Revisar logs en Airflow"
)
requests.post(WEBHOOK_URL, json={"message": message})
# INICIO
log('STARTING', f'Inicializando Generacion de URL de {db_database}')
airflow_logger.info(f"{''*50}")
airflow_logger.info(f"▶ START | {db_database} | folder: {code_folder}")
#print(f"{'─'*50}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ▶ START | {db_database} | folder: {code_folder}")
# LEER FTP
try:
ftp_data = read_ftp(ftp_server, ftp_user, ftp_password,
code_folder, identity_id, year_folder, month_folder)
log('SUCCESS', f'Lectura FTP folder {code_folder} exitosa')
airflow_logger.info(f"✅ FTP | {db_database} | {len(ftp_data)} archivos en {code_folder}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ FTP | {db_database} | {len(ftp_data)} archivos encontrados en {code_folder}")
except Exception as e:
log('FAILED', f'Error leyendo FTP folder {code_folder}', extra_info=e)
send_whatsapp(f'Error leyendo FTP folder {code_folder} para {db_database}', e)
airflow_logger.error(f"❌ FTP | {db_database} | {e}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ FTP | {db_database} | {e}")
continue
# LEER SQL
try:
sql_data = read_table_where(db_database, db_password, db_user, db_server, db_table, code_folder)
saved_ids = {r['IDcodigo'] for r in sql_data}
log('SUCCESS', f'Lectura URLs de {db_database} exitosa')
airflow_logger.info(f"✅ SQL | {db_database} | {len(saved_ids)} registros existentes")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ SQL | {db_database} | {len(saved_ids)} registros existentes")
except Exception as e:
log('FAILED', f'Error leyendo URLs de {db_database}', extra_info=e)
send_whatsapp(f'Error leyendo URLs de {db_database} para comparar con FTP.', e)
airflow_logger.error(f"❌ SQL | {db_database} | {e}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ SQL | {db_database} | {e}")
continue
# INSERTAR
try:
to_insert = [r for r in ftp_data if r['IDcodigo'] not in saved_ids]
info_insert = f'{db_database} → FTP: {len(ftp_data)} | SQL: {len(saved_ids)} | A insertar: {len(to_insert)}'
airflow_logger.info(f"️ DIFF | {db_database} | FTP: {len(ftp_data)} | SQL: {len(saved_ids)} | A insertar: {len(to_insert)}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ️ DIFF | {db_database} | FTP: {len(ftp_data)} | SQL: {len(saved_ids)} | A insertar: {len(to_insert)}")
if to_insert:
inserted = insert_records(db_database, db_password, db_user,
db_server, db_table, to_insert)
log('SUCCESS', f'Insercion en {db_database} exitosa', extra_info=f'| FTP: {len(ftp_data)} | SQL: {len(saved_ids)} | Insertados: {len(to_insert)}')
airflow_logger.info(f"✅ INSERT | {db_database} | FTP: {len(ftp_data)} | SQL: {len(saved_ids)} | Insertados: {len(to_insert)}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ INSERT | {db_database} | FTP: {len(ftp_data)} | SQL: {len(saved_ids)} | Insertados: {len(to_insert)}")
else:
log('SUCCESS', f'No hay registros nuevos para insertar en {db_database}')
airflow_logger.warning(f"⚠️ INSERT | {db_database} | Sin registros nuevos para insertar")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ⚠️ INSERT | {db_database} | Sin registros nuevos para insertar")
except Exception as e:
log('FAILED', f'Error insertando en {db_database}', extra_info=e)
send_whatsapp(f'Error insertando URLs en {db_database}.', e)
airflow_logger.error(f"❌ INSERT | {db_database} | {e}")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ INSERT | {db_database} | {e}")
continue
# FIN
log('FINISHED', f'Finalizando Generacion de URL de {db_database}')
airflow_logger.info(f"⏹ END | {db_database} | proceso completado")
#print(f"[{datetime.now().strftime('%H:%M:%S')}] ⏹ END | {db_database} | proceso completado")
#FINALIZACION DEL PROGRAMA - CIERRE DE LOGGER Y MENSAJES FINALES
kafka_logger.close()
airflow_logger.info(f"{'='*50}")
airflow_logger.info(f"⏹ FINALIZADO {APP_NAME} {VERSION}")
airflow_logger.info(f"{'='*50}")
#print(f"{'='*50}")
#print(f"⏹ FINALIZADO {APP_NAME} {VERSION}")
#print(f"{'='*50}")
#run_save_url()