first commit

This commit is contained in:
Eidan T.
2026-05-09 14:00:04 -04:00
commit e0ec11977c
42 changed files with 4596 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Project specific
Logs/
Reportes/
Test/
tests/test_*.log
*.tmp
*.bak
# Microsoft Office files
*.doc
*.docx
*.xls
*.xlsx
*.ppt
*.pptx
*.accdb
*.mdb
*.pub
*.one
*.pps
*.ppsx
*.pot
*.potx
*.vsd
*.vsdx
*.mpp
*.xsn
*.xsf
# Office temporary files
~$*.doc*
~$*.xls*
~$*.ppt*
# Database files
*.db
*.sqlite
*.sqlite3
# Secrets and credentials
secrets.json
credentials.json
config.local.py
# Docker
.dockerignore
# Temporary files
*.tmp
*.temp
.cache/
.temp/
+47
View File
@@ -0,0 +1,47 @@
import pytz
from datetime import datetime, timedelta, time
class ConfigDatabase:
db_host= 'sql.glmanalytics.com'
db_user = 'vmglm001'
db_password = "A16n)J@^fj<]G('{"
class ConfigFTP:
ftp_server = '000oqta.rcomhost.com'
ftp_user = "ftp3592287"
ftp_password = 'mpFFMRW4W9w@SuK'
ftp_port = 21
ftp_path = "/www/TranferData_APP_PYTHON/"
class ConfigAPI:
pass
class GeneralConfig:
#DataControl
numDays = 3
zoneTime = pytz.timezone("America/El_Salvador")
zoneTimeCP = pytz.timezone("America/Santo_Domingo")
cutTime = time(17, 30, 00)
current_time = datetime.now(zoneTime)
current_hour = current_time.hour
nowTime = current_time.strftime("%Y-%m-%d")
nowTimeFormat = current_time.strftime("%Y%m%d")
execution_time = datetime.now(zoneTime)
#time = datetime.now(zoneTime)
#Para emails
fromEmail = 'notifications.noreply@gomezleemarketing.com'
smtp_server="smtp.gmail.com"
smtp_port=587
passwordEmail = 'GLM123456new1'
class ConfigNotifications:
webhookURL_wps_notifcation = "https://automation.glmanalytics.com/webhook/wsp-notifcation"
webhookURL = "https://automation.glmanalytics.com/webhook/205ae0d5-fc9c-44da-951c-84c033236579" # Ajusta con tu URL real
+9
View File
@@ -0,0 +1,9 @@
#from .GeneralConfig import *
#from .corripioConfig import *
#from .corripioConfig2 import *
#from .dipoConfig import *
#from .pgConfig import *
#from .feduroConfig import *
#from .cimbertonConfig import *
#from .echamorroConfig import *
#from .ftp_service import *
+42
View File
@@ -0,0 +1,42 @@
import os
class FeduroConfig:
userSharepoint = 'feddatadeployment.im@pg.com' #os.getenv('sharepointPG_user')
passwordSharepoint = 'Feduro_PG_2025$' #= os.getenv('sharepointPG_password')
siteSharepoint = 'https://pgone.sharepoint.com/sites/feduroinfinitystores' #os.getenv('sharepointPG_site')
database = 'IS001'
databaseID = '00010007'
areaID = '0007'
project_name = 'FEDURO'
classID = '0003'
reportsID = {'Cobertura':'0001',
'NegociadasIS001':'0002',
'Promociones':'0003',
#'Precios':'0004',
'Fmot':'0005',
'QualityDisplay':'0006',
'Checkout':'0007',
'QualityShelf':'0008',
'SOSFeduro':'0009',
#'OsaTorre':'0010',
#'OsaPsmt':'0011',
#'OsaFarmacia':'0012',
'OsaFotografico':'0013',
#'DataAvailability':'0014',
'OsaAnalogo':'0015',
'ModuloTareas':'0016',
#'SOSAPDO_IS001':'0017',
#'SOSBabyCare_IS001':'0018',
#'SOSFabricCare_IS001':'0019',
#'SOSFamilyCare_IS001':'0020',
#'SOSFemCare_IS001':'0012',
#'SOSHairCare_IS001':'0022',
#'SOSHomeCare_IS001':'0023',
#'SOSOralCare_IS001':'0024',
#'SOSPHC_IS001':'0025'
}
+115
View File
@@ -0,0 +1,115 @@
import os
from App.Config.GeneralConfig import GeneralConfig
from App.Config.GeneralConfig import ConfigFTP
from App.Utils.db_utils import DatabaseManager
from App.Utils.ftp_utils import FTPManager
from App.Utils.sftp_utils import SFTPManager
class FeduroConfig:
def __init__(self):
#Inicializando la base de datos
self.databaseCode = 'IS001'
self.databaseID = '00010007'
self.database = DatabaseManager(self.databaseCode, self.databaseID)
#Inicialzando el FTP
self.ftpdatabase = FTPManager(ConfigFTP.ftp_server, ConfigFTP.ftp_user, ConfigFTP.ftp_password, ConfigFTP.ftp_port)
#Inicializando el SFTP
self.sftp_host = 'c6a3nxpth6mf4.eastus2.azurecontainer.io'
self.sftp_port = 22
self.sftp_username = 'eLeader01'
self.sftp_password = 'c8gNBhJE@zt$ZkT&'
self.sftpdatabase = SFTPManager(self.sftp_host, self.sftp_port, self.sftp_username, self.sftp_password)
#Inicializando SharePoint
self.userSharepoint = 'feddatadeployment.im@pg.com' #os.getenv('sharepointPG_user')
self.passwordSharepoint = 'Feduro_PG_2025$' #= os.getenv('sharepointPG_password')
self.siteSharepoint = 'https://pgone.sharepoint.com/sites/feduroinfinitystores' #os.getenv('sharepointPG_site')
#General configuration feduro
self.areaID = '0007'
self.project_name = 'FEDURO'
self.classID = '0003'
self.reports = self.database.get_execute_query(f"select * from TransferData where active = 1")
self.reports_sftp = {'Cobertura':'/upload/FEDURO/DataCollection/Cobertura/CoberturaCheckIn-Out/',
'NegociadasIS001':'/upload/FEDURO/DataCollection/Negociadas/AC Ejecutado/',
'Promociones':'/upload/FEDURO/DataCollection/Promociones/PROMO EJECUTADA/',
'Precios':'',
'Fmot':'/upload/FEDURO/DataCollection/Fmot/fmot Data/',
'QualityDisplay':'/upload/FEDURO/DataCollection/Quality Display/',
'Checkout':'/upload/FEDURO/DataCollection/CheckOut/Data/',
'QualityShelf':'/upload/FEDURO/DataCollection/QualityShelf/QualityShelf_Data/',
'SOSFeduro':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'OsaTorre':'',
'OsaPsmt':'',
'OsaFarmacia':'',
'OsaFotografico':'/upload/FEDURO/DataCollection/OSA/OSA_Fotos_Categoria/',
'DataAvailability':'',
'OsaAnalogo': '/upload/FEDURO/DataCollection/OSA/Data/',
'ModuloTareas': '/upload/FEDURO/DataCollection/ModuloTareas/Data/',
'SOSAPDO_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSBabyCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSFabricCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSFamilyCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSFemCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSHairCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSHomeCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSOralCare_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/',
'SOSPHC_IS001':'/upload/FEDURO/DataCollection/SOS/Share of Share Ejecutado/'
}
self.reportsID = {'Cobertura':'0001',
'NegociadasIS001':'0002',
'Promociones':'0003',
#'Precios':'0004',
'Fmot':'0005',
'QualityDisplay':'0006',
'Checkout':'0007',
'QualityShelf':'0008',
'SOSFeduro':'0009',
#'OsaTorre':'0010',
#'OsaPsmt':'0011',
#'OsaFarmacia':'0012',
'OsaFotografico':'0013',
#'DataAvailability':'0014',
'OsaAnalogo':'0015',
'ModuloTareas':'0016',
#'SOSAPDO_IS001':'0017',
#'SOSBabyCare_IS001':'0018',
#'SOSFabricCare_IS001':'0019',
#'SOSFamilyCare_IS001':'0020',
#'SOSFemCare_IS001':'0012',
#'SOSHairCare_IS001':'0022',
#'SOSHomeCare_IS001':'0023',
#'SOSOralCare_IS001':'0024',
#'SOSPHC_IS001':'0025'
}
self.steps = {
"sftp": True,
"ftp": True,
"sharepoint_python": True,
"sharepoint_N8N": False,
}
+79
View File
@@ -0,0 +1,79 @@
import os
from App.Config.GeneralConfig import GeneralConfig
from App.Config.GeneralConfig import ConfigFTP
from App.Utils.db_utils import DatabaseManager
from App.Utils.ftp_utils import FTPManager
from App.Utils.sftp_utils import SFTPManager
from App.Utils.sharepoint_utils import SharepointManager
class PGConfig:
def __init__(self):
#Inicializando la base de datos
self.databaseCode = 'glm_pg'
self.databaseID = '00080001'
self.database = DatabaseManager(self.databaseCode, self.databaseID)
#Inicializando SharePoint
self.userSharepoint = 'gldatadeployment.im@pg.com'
self.passwordSharepoint = 'Glm-infinity-2025'
self.siteSharepoint = 'https://pgone.sharepoint.com/sites/eLeaderDataSharing'
data = self.database.get_execute_query("select reportID, parameter from TransferDataParameter where category = 'Sharepoint'")
self.sharepointPath = {
row['reportID']: row['parameter']
for row in data
}
self.sharepoint = SharepointManager(self.siteSharepoint, self.userSharepoint, self.passwordSharepoint)
#Inicialzando el FTP
self.ftpdatabase = FTPManager(ConfigFTP.ftp_server, ConfigFTP.ftp_user, ConfigFTP.ftp_password, ConfigFTP.ftp_port)
#Inicializando el SFTP
#self.sftp_host = 'c6a3nxpth6mf4.eastus2.azurecontainer.io'
#self.sftp_port = 22
#self.sftp_username = 'eLeader01'
#self.sftp_password = 'c8gNBhJE@zt$ZkT&'
#self.sftpdatabase = SFTPManager(self.sftp_host, self.sftp_port, self.sftp_username, self.sftp_password)
#General configuration PG
self.areaID = '0001'
self.project_name = 'PG'
self.classID = '0003'
self.reports = self.database.get_execute_query(f"select * from TransferData where active = 1")
self.reportsID = {'Cobertura':'0001',
'Negociadas':'0002',
'Promociones':'0003',
#'Precios':'0004',
#'Fmot':'0005',
'QualityDisplay':'0006',
'Checkout':'0007',
'QualityShelf':'0008',
'SOS':'0009',
#'OsaTorre':'0010',
#'OsaPsmt':'0011',
'OsaFarmacia':'0012',
'OsaMassy':'0013',
'OsaPenny':'0014',
'OsaFotografico':'0015',
'DataAvailability':'0016'
}
+3
View File
@@ -0,0 +1,3 @@
APP_NAME = "PYTHON_TRANSFERDATA_APP"
VERSION = "2.0.0"
AUTHOR = "eurbina"
+2
View File
@@ -0,0 +1,2 @@
# Modelos de datos (ej. esquemas de tablas si usas ORM)
# Por ahora vacío; agrega clases si usas SQLAlchemy
+1
View File
@@ -0,0 +1 @@
#from .etl_service import *
+23
View File
@@ -0,0 +1,23 @@
# Servicio API: endpoints GET para descarga
from flask import Flask, request, jsonify
from App.Services.logic_service import run_etl # O importa db_utils directamente
import pandas as pd
import logging
app = Flask(__name__)
logging.basicConfig(filename='Logs/api.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
@app.route('/api/download', methods=['GET'])
def download_data():
try:
project = request.args.get('project')
task = request.args.get('task')
# Lógica: query DB y devolver JSON/CSV
conn = get_db_connection() # Importa de Utils
query = f"SELECT * FROM {task} WHERE project = '{project}'"
df = pd.read_sql(query, conn)
conn.close()
return jsonify(df.to_dict(orient='records'))
except Exception as e:
logging.error(f"API falló: {str(e)}")
return jsonify({"error": "Error interno del servidor"}), 500 # Devuelve error, pero Flask sigue vivo
+339
View File
@@ -0,0 +1,339 @@
import sys
import os
from typeguard import config
# subir 2 niveles hasta la raíz (donde está App)
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from App.Utils.db_utils import DatabaseManager # Reutilizado de db_utils.py
from App.Config.GeneralConfig import GeneralConfig # Reutilizado de GeneralConfig.py
from App.Config.pgConfig import PGConfig # Reutilizado de PGConfig.py
from App.Config.version import APP_NAME, VERSION, AUTHOR # Reutilizado de version.py
from App.Utils.report_utils import ReportGenerator # Nueva utilidad para generar reportes (ej. Excel)
import logging
from datetime import datetime
# Configura logging
logging.basicConfig(filename='Logs/etl.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
def run_etl(config, data_report, target_date_report):
'''Función principal para ejecutar ETL.
Lee los datos desde la base de datos, los transforma en un excel y los sube a SharePoint.
Args:
config: Configuración general de la aplicación.
database: Instancia de la clase DatabaseManager.
data_report: Diccionario con la información del reporte a generar.
target_date_report: Fecha objetivo para filtrar datos (str, formato 'YYYY-MM-DD')
Returns:
Tuple[bool, str]: Una tupla con el resultado de la operación y un mensaje informativo.
'''
date_execution = datetime.now(GeneralConfig.zoneTime)
target_date_report_format = target_date_report.strftime('%Y-%m-%d')
result = {
"errors": []
} # Lista para almacenar errores y mensajes informativos
################################## INICIANDO REPORTE ##################################
config.database.log_execution(procedure_id=str(config.database.db_databaseID)+str('0003')+str('0189')+str(data_report['reportID']),
procedure_name='Trasmision Datos '+str(data_report['project_name'])+' '+str(data_report['report_name']),
status='STARTING',
message='Inicializando la trasmision de reporte: '+str(data_report['report_name']),
executed_by=str(config.database.db_user),
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=str(date_execution))
################################ OBTENIENDO DATOS DE LA BASE DE DATOS ##################################
try:
query = f"SELECT * FROM {data_report['Table_Name']} WHERE Fecha = '{target_date_report_format}'"
print(query)
data_sql_report = config.database.get_execute_query(query)
if not data_sql_report:
print(f"No se encontraron datos para el reporte {data_report['report_name']} en la fecha {target_date_report_format}")
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'WARNING',
message=f"No se encontraron datos para el reporte {data_report['report_name']} en la fecha {target_date_report_format}",
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
return {
"status": "WARNING",
"report": data_report["report_name"],
"message": "Sin datos en la base de datos"
}
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'SUCCESS',
message=f'Lectura de la base datos para crear reporte :{data_report["report_name"]}',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
except Exception as e:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'FAILED',
message=f'Error en la lectura de la base datos para crear reporte :{data_report["report_name"]}',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=str(e),
target_date=target_date_report_format,
date=f'{date_execution}')
return {
"status": "FAILED",
"report": data_report["report_name"],
"message": f"Error lectura BD {str(e)}"
}
################################ CREANDO REPORTE EN EXCEL ##################################
try:
doc_report = ReportGenerator(target_date_report,**data_report)
doc_report.createReportData(data_sql_report)
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'SUCCESS',
message=f'Creación del reporte :{data_report["report_name"] } en formato Excel',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
except Exception as e:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'FAILED',
message=f'Error en la creación del reporte :{data_report["report_name"] } en formato Excel',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=str(e),
target_date=target_date_report_format,
date=f'{date_execution}')
return {
"status": "FAILED",
"report": data_report["report_name"],
"message": f"Error creación del reporte {str(e)}"
}
################################ SUBIR REPORTE A FTP ###############################
if data_report.get('FTP', False):
try:
ftp_path = f"/www/TranferData_APP_PYTHON/{config.project_name}/{data_report['report_name']}/"
#print(ftp_path)
config.ftpdatabase.uploadFile(ftp_path, doc_report.docsend_Path) # Asegúrate de que la configuración FTP esté presente en el config general
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'SUCCESS',
message=f'Subida del reporte :{data_report["report_name"] } a FTP',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
except Exception as e:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'FAILED',
message=f'Error en la subida del reporte :{data_report["report_name"] } a FTP',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=str(e),
target_date=target_date_report_format,
date=f'{date_execution}')
result["errors"].append(f"Error subir reporte a FTP {str(e)}")
#return {
# "status": "FAILED",
# "report": data_report["report_name"],
# "message": f"Error subir reporte a FTP {str(e)}"
#}
############################### SUBIR REPORTA A SFTP ##################################
if data_report.get('SFTP', False):
try:
# Lógica para subir a SFTP (puedes usar paramiko o alguna librería similar)
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'SUCCESS',
message=f'Subida del reporte :{data_report["report_name"] } a SFTP',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
except Exception as e:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'FAILED',
message=f'Error en la subida del reporte :{data_report["report_name"] } a SFTP',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=str(e),
target_date=target_date_report_format,
date=f'{date_execution}')
result["errors"].append(f"Error subir reporte a SFTP {str(e)}")
#return {
# "status": "FAILED",
# "report": data_report["report_name"],
# "message": f"Error subir reporte a SFTP {str(e)}"
#}
################################ SUBIR REPORTE A SHAREPOINT PYTHON ##################################
if data_report.get('SharePoint', False):
try:
sharepoint_folder = config.sharepointPath.get(data_report['reportID'])
if not sharepoint_folder:
raise Exception(f"No existe ruta SharePoint para reportID {data_report['reportID']}")
config.sharepoint.uploadFile(doc_report, sharepoint_folder)
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'SUCCESS',
message=f'Subida del reporte :{data_report["report_name"] } a SharePoint usando Python',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
except Exception as e:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'FAILED',
message=f'Error en la subida del reporte :{data_report["report_name"] } a SharePoint usando Python',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=str(e),
target_date=target_date_report_format,
date=f'{date_execution}')
result["errors"].append(f"Error subir reporte a SharePoint usando Python {str(e)}" )
#return {
# "status": "FAILED",
# "report": data_report["report_name"],
# "message": f"Error subir reporte a SharePoint usando Python {str(e)}"
#}
################################ SUBIR REPORTE A SHAREPOINT N8N ##################################
if data_report.get('N8N', False):
try:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'SUCCESS',
message=f'Subida del reporte :{data_report["report_name"] } a SharePoint usando N8N',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=None,
target_date=target_date_report_format,
date=f'{date_execution}')
except Exception as e:
config.database.log_execution(procedure_id=f'{config.database.db_databaseID}00030189{data_report["reportID"]}',
procedure_name=f'Trasmision Datos {data_report["project_name"]} {data_report["report_name"]}',
status=f'FAILED',
message=f'Error en la subida del reporte :{data_report["report_name"] } a SharePoint usando N8N',
executed_by=f'{config.database.db_user}',
source= f'{APP_NAME}_{VERSION}',
extra_info=str(e),
target_date=target_date_report_format,
date=f'{date_execution}')
result["errors"].append(f"Error subir reporte a SharePoint usando N8N {str(e)}")
#return {
# "status": "FAILED",
# "report": data_report["report_name"],
# "message": f"Error subir reporte a SharePoint usando N8N {str(e)}"
#}
#################################################################################################
if result["errors"]:
return {
"status": "COMPLETED_WITH_ERRORS",
"report": data_report["report_name"],
"message": " | ".join(result["errors"])
}
else:
return {
"status": "SUCCESS",
"report": data_report["report_name"],
"message": "ETL ejecutado exitosamente"
}
# Aquí va la lógica de ETL específica para cada proyecto y reporte
# Por simplicidad, loggea que se ejecutó
#logging.info(f"ETL ejecutado para proyecto: {project}, reporte: {report_name}, usando config: {config.project_name}")
"""
pg_config = PGConfig()
database = DatabaseManager(pg_config.database, pg_config.databaseID)
data_report = {
'project_name': 'PG',
'report_name': 'Cobertura',
'Table_Name': 'ReportCobertura',
'nowTime': GeneralConfig.nowTime,
'nowTimeFormat': GeneralConfig.nowTimeFormat,
'project_name': pg_config.project_name,
'current_time': GeneralConfig.execution_time,
'reportID': '0001'
}
run_etl(pg_config, database, data_report, target_date_report='2026-03-26')"""
+13
View File
@@ -0,0 +1,13 @@
# Servicio UI: manejo de forms web
from flask import render_template, request
from App.Services.logic_service import run_etl
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
project = request.form['project']
task = request.form['task']
# Trigger ETL manual
run_etl(project, task)
return "ETL ejecutado"
return render_template('index.html')
+3
View File
@@ -0,0 +1,3 @@
/* Estilos básicos para UI */
body { font-family: Arial; }
form { max-width: 400px; }
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head><title>DataTransfer UI</title></head>
<body>
<h1>Actualizar Datos</h1>
<form method="POST">
Proyecto: <select name="project"><option>Feduro</option><option>Comberton</option></select><br>
Tarea: <select name="task"><option>negociadas</option><option>promociones</option></select><br>
Fecha inicio: <input type="date" name="start_date"><br>
Fecha fin: <input type="date" name="end_date"><br>
<button type="submit">Actualizar</button>
</form>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
SELECT TOP (1000) [ID]
,[ExecutionDate]
,[ReportName]
,[ReportID]
,[Active]
,[TableName]
FROM [glm_pg].[dbo].[TransferDataParameters]
CREATE TABLE TransferData (
ID INT IDENTITY(1,1) PRIMARY KEY,
ExportDate DATETIME DEFAULT GETDATE(),
ReportID AS RIGHT('0000' + CAST(ID AS VARCHAR), 4) PERSISTED, -- 👈 automático
ReportName VARCHAR(100) NOT NULL,
TableName VARCHAR(200) NOT NULL, -- 👈 AQUÍ lo importante
Active BIT NOT NULL DEFAULT 1,
FTP BIT NOT NULL DEFAULT 1,
FTPS BIT NOT NULL DEFAULT 1,
SharePoint BIT NOT NULL DEFAULT 1,
N8N BIT NOT NULL DEFAULT 0
);
INSERT INTO TransferData (ReportName, TableName, Active, FTP, FTPS, SharePoint, N8N)
VALUES
-- 0001
('Cobertura','ReportCobertura',1,1,1,1,0),
-- 0002
('NegociadasIS001','IS001.dbo.AcuerdosComerciales_Report',1,1,1,1,0),
-- 0003
('Promociones','PromocionesAC_Report',1,1,1,1,0),
-- 0004 (desactivado)
('Precios','s/d',0,1,1,1,0),
-- 0005
('Fmot','Fmot_Report',1,1,1,1,0),
-- 0006
('QualityDisplay','dbo.QualityDisplay_Report',1,1,1,1,0),
-- 0007
('Checkout','IS001.dbo.Checkout_Report',1,1,1,1,0),
-- 0008
('QualityShelf','QualityShelf_Report',1,1,1,1,0),
-- 0009
('SOSFeduro','View_Medicion_SOS_PG',1,1,1,1,0),
-- 0010 (desactivado)
('OsaTorre','s/d',0,1,1,1,0),
-- 0011 (desactivado)
('OsaPsmt','s/d',0,1,1,1,0),
-- 0012 (desactivado)
('OsaFarmacia','s/d',0,1,1,1,0),
-- 0013
('OsaFotografico','OsaManualFotos_Report',1,1,1,1,0),
-- 0014 (desactivado)
('DataAvailability','s/d',0,1,1,1,0),
-- 0015
('OsaAnalogo','OsaManualAnalogo_Report',1,1,1,1,0),
-- 0016
('ModuloTareas','ModuloTareas_Report',1,1,1,1,0),
-- 0017 (desactivado)
('SOSAPDO_IS001','s/d',0,1,1,1,0),
-- 0018 (desactivado)
('SOSBabyCare_IS001','s/d',0,1,1,1,0),
-- 0019 (desactivado)
('SOSFabricCare_IS001','s/d',0,1,1,1,0),
-- 0020 (desactivado)
('SOSFamilyCare_IS001','s/d',0,1,1,1,0),
-- 0021 (desactivado)
('SOSFemCare_IS001','s/d',0,1,1,1,0),
-- 0022 (desactivado)
('SOSHairCare_IS001','s/d',0,1,1,1,0),
-- 0023 (desactivado)
('SOSHomeCare_IS001','s/d',0,1,1,1,0),
-- 0024 (desactivado)
('SOSOralCare_IS001','s/d',0,1,1,1,0),
-- 0025 (desactivado)
('SOSPHC_IS001','s/d',0,1,1,1,0);
select * from TransferData
select * from TransferData where active = 1
UPDATE TransferData
SET Active = 0
WHERE ReportID = '0005'
--drop table TransferData
+5
View File
@@ -0,0 +1,5 @@
#from .db_utils import *
#from .report_utils import *
#from .sharepoint_utils import *
#from .ftp_utils import *
#from .sftp_utils import *
+134
View File
@@ -0,0 +1,134 @@
# Utilidades para conexión a BD - Reutilizado y ajustado de database.py v1
import pyodbc
from App.Config.GeneralConfig import ConfigDatabase # Ajusta si GeneralConfig tiene otra estructura
class DatabaseManager:
def __init__(self, database, databaseID):
self.db_database = database
self.db_user = ConfigDatabase.db_user
self.db_password = ConfigDatabase.db_password
self.db_host = ConfigDatabase.db_host
self.db_databaseID = databaseID
def _connect(self):
try:
self.conn_str = (
'DRIVER={ODBC Driver 18 for SQL Server};'
f'SERVER={self.db_host};'
f'DATABASE={self.db_database};'
f'UID={self.db_user};'
f'PWD={self.db_password};'
f'Encrypt=yes;'
f'TrustServerCertificate=yes;'
)
#print(self.conn_str)
self.connection = pyodbc.connect(self.conn_str)
except pyodbc.Error as e:
print(f"Error al conectar a la base de datos: {e}")
self.connection = None
def _disconnect(self):
if self.connection:
self.connection.close()
# Función para ejecutar queries que retornan valores
def get_execute_query(self, query):
self._connect()
if not self.connection:
return None, "Error de conexión"
self.cursor = self.connection.cursor()
self.cursor.execute(query)
columns = [col[0] for col in self.cursor.description]
data = self.cursor.fetchall()
results = [dict(zip(columns, row)) for row in data]
self.cursor.close()
self._disconnect()
return results
# Función para ejecutar queries que retornan valores
def get_execute_query2(self, query):
self._connect()
if not self.connection:
return None, "Error de conexión"
try:
self.cursor = self.connection.cursor()
self.cursor.execute(query)
columns = [col[0] for col in self.cursor.description]
data = self.cursor.fetchall()
results = [dict(zip(columns, row)) for row in data]
self.cursor.close()
return results, None
except Exception as e:
print(f"Error al ejecutar la consulta SELECT: {e} \n {query}")
return None, str(e)
finally:
self._disconnect()
# Función para ejecutar queries que no retornan valores
def set_execute_query(self, query):
self._connect()
if not self.connection:
return False
try:
cursor = self.connection.cursor()
cursor.execute(query)
self.connection.commit()
cursor.close()
return True
except Exception as e:
print(f"Error al ejecutar la consulta de modificación: {e}")
return False
finally:
self._disconnect()
def log_execution(self, procedure_id, procedure_name, status, message, executed_by, extra_info, target_date, date,
procedure_type='PYTHON_APP', source='PYTHON_TRANSFERDATA_APP'):
self._connect()
if not self.connection:
return
self.cursor = self.connection.cursor()
sql = """
DECLARE @ExecID VARCHAR(50), @FieldID VARCHAR(50);
EXEC dbo.InsertExecutionLog
@ProcedureID = ?,
@ProcedureName = ?,
@ProcedureType = ?,
@Date = ?,
@TargetDate = ?,
@Status = ?,
@ExecutedBy = ?,
@MessageDescription = ?,
@Source = ?,
@ExtraInfo = ?,
@ExecutionIDGenerated = @ExecID OUTPUT,
@FieldIDGenerated = @FieldID OUTPUT;
SELECT @ExecID AS ExecutionID, @FieldID AS FieldID;
"""
try:
self.cursor.execute(sql, (
procedure_id, procedure_name, procedure_type,
date, target_date, status,
executed_by, message, source, extra_info
))
row = self.cursor.fetchone()
self.connection.commit()
except Exception as e:
print(f"Error en log_execution: {e}")
finally:
self._disconnect()
# Función simple para compatibilidad (usa DatabaseManager internamente)
def get_db_connection(database='tu_db', databaseID='default'):
manager = DatabaseManager(database, databaseID)
manager._connect()
return manager.connection # Devuelve la conexión cruda para pandas, etc.
+133
View File
@@ -0,0 +1,133 @@
from ftplib import FTP
import os
import time
class FTPManager:
def __init__(self, siteFTP, userFTP, passwordFTP, postFTP):
self.server = siteFTP
self.user = userFTP
self.password = passwordFTP
self.port = postFTP # Puerto por defecto: 21
self.ftp_client = None
self.list_failed_uploads = []
#def __init__(self, **kwargs):
# self.server = kwargs.get('server')
# self.user = kwargs.get('user')
# self.password = kwargs.get('password')
# self.port = kwargs.get('port', 21) # Puerto por defecto: 21
# self.ftp_client = None
# self.list_failed_uploads = []
def connect(self):
"""Conecta al servidor FTP"""
try:
self.ftp_client = FTP()
self.ftp_client.connect(self.server, self.port)
self.ftp_client.login(self.user, self.password)
self.ftp_client.encoding = 'latin-1'
#print(f"Conexión exitosa al servidor FTP: {self.server}")
except Exception as e:
#print(f"Error al conectar al servidor FTP: {e}")
raise
def disconnect(self):
"""Cierra la conexión FTP"""
if self.ftp_client:
self.ftp_client.quit()
#print("Conexión cerrada con éxito")
def upload_photos(self, local_folder, remote_folder, batch_size=100):
"""
Sube archivos en lotes de 100. Si hay un error, omite el archivo y lo guarda para intentarlo al final.
"""
try:
self.ftp_client.cwd(remote_folder)
except:
# if not exit create folder
self.ftp_client.mkd(remote_folder)
self.ftp_client.cwd(remote_folder)
files = [f for f in os.listdir(local_folder) if os.path.isfile(os.path.join(local_folder, f))]
total_files = len(files)
print(f"Total de archivos a subir: {total_files}")
for i in range(0, total_files, batch_size):
batch = files[i:i+batch_size]
print(f"Subiendo lote {i//batch_size + 1}: {len(batch)} archivos")
for file in batch:
local_path = os.path.join(local_folder, file)
try:
with open(local_path, 'rb') as local_file:
self.ftp_client.storbinary(f"STOR {file}", local_file)
print(f"Subido: {file}")
except Exception as e:
print(f"Error al subir {file}: {e}")
self.list_failed_uploads.append(file)
time.sleep(2) # Pequeña pausa entre lotes para estabilidad
# Intentar subir nuevamente los fallidos
if self.list_failed_uploads:
print("Reintentando archivos fallidos...")
for file in self.list_failed_uploads:
local_path = os.path.join(local_folder, file)
try:
with open(local_path, 'rb') as local_file:
self.ftp_client.storbinary(f"STOR {file}", local_file)
print(f"Subido en reintento: {file}")
except Exception as e:
print(f"Fallo definitivo en {file}: {e}")
print("Proceso de subida finalizado.")
def uploadFile(self, remote_folder, local_file_path):
self.connect()
try:
self.ftp_client.cwd("/") # 👈 IMPORTANTE
self.ensure_remote_path(remote_folder)
with open(local_file_path, "rb") as f:
file_name = os.path.basename(local_file_path)
self.ftp_client.storbinary(f"STOR {file_name}", f)
finally:
self.disconnect()
def ensure_remote_path(self, remote_folder):
folders = remote_folder.strip("/").split("/")
# SIEMPRE empezar desde raíz
self.ftp_client.cwd("/")
for folder in folders:
if folder == "":
continue
try:
self.ftp_client.cwd(folder)
except:
self.ftp_client.mkd(folder)
#print(f"Creando/navegando carpeta: {folder}")
self.ftp_client.cwd(folder)
# Ejemplo de uso
if __name__ == "__main__":
config = {
"server": "glmservice.com",
"user": "ftpeleaderuser",
"password": "AdAw#BaAjwQdc4B",
"port": 21
}
ftp_manager = FTPManager(**config)
try:
ftp_manager.connect()
ftp_manager.upload_photos("C:\\Users\\emili\\Downloads\\sfotos", "/imglm/test/glmpg01/")
finally:
ftp_manager.disconnect()
+50
View File
@@ -0,0 +1,50 @@
import requests
from datetime import datetime, timedelta
from App.Config.GeneralConfig import ConfigNotifications
def send_whatsapp(message):
url = ConfigNotifications.webhookURL_wps_notifcation # URL del webhook de WhatsApp en WPS
payload = {
"message": message
}
requests.post(url, json=payload)
def build_message(project_name, summary, duration):
status = "FINALIZADO" if not summary["failed"] else "FINALIZADO CON ERRORES ❌"
msg = f"""PROYECTO: {project_name}
Fecha: {datetime.now().strftime('%Y-%m-%d')}
Estado: {status}
Resumen:
- Reportes procesados: {summary['total']}
- Exitosos: {summary['success']}
- Con errores: {len(summary['failed'])}
"""
if summary["failed"]:
msg += "❌ Errores críticos:\n"
for e in summary["failed"]:
msg += f"- {e}\n"
if summary["errors"]:
msg += "\n🟠 Errores no bloqueantes:\n"
for e in summary["errors"]:
msg += f"- {e}\n"
if summary["warnings"]:
msg += "\n⚠️ Advertencias:\n"
for w in summary["warnings"]:
msg += f"- {w}\n"
msg += f"\n🕐 Duración: {str(duration).split('.')[0]}"
return msg
+53
View File
@@ -0,0 +1,53 @@
# Utilidades para generación de reportes - Reutilizado y ajustado de report.py v1
import os
import pandas as pd
import logging
from datetime import datetime
# Configura logging
logging.basicConfig(filename='Logs/reports.log', level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
class ReportGenerator:
"""Clase para generar y exportar reportes en Excel"""
def __init__(self, target_date_report, **kwargs ):
"""
Inicializa generador de reportes
Args:
name: Nombre del reporte
nowTime: Fecha actual (datetime)
nowTimeFormat: Formato de fecha (str)
project_name: Nombre del proyecto
current_time: Tiempo de ejecución (datetime)
reportID: ID del reporte (str)
"""
self.name = kwargs.get('report_name',None)
#self.date = kwargs.get('nowTime',None)
self.dateFormat = target_date_report.strftime("%Y%m%d")
self.project_name = kwargs.get('project_name',None)
#self.current_time = kwargs.get('current_time',None)
#self.reportID = kwargs.get('reportID',None)
self.actual_path = os.path.dirname(os.path.abspath(__file__))
#self.download_dir = os.path.join(self.actual_path, '..', '..','Reportes', str(self.project_name), str(self.name), 'download')
self.docsend_dir = os.path.join(self.actual_path, '..', '..','Reportes', str(self.project_name), str(self.name))
#os.makedirs(self.download_dir, exist_ok=True)
os.makedirs(self.docsend_dir, exist_ok=True)
#self.dowload_path = os.path.normpath(self.download_dir)
self.docsend_Path = os.path.normpath(os.path.join(self.docsend_dir, f"{self.name} - {self.dateFormat}.xlsx"))
self.report_name = f"{self.name} - {self.dateFormat}.xlsx"
print(self.report_name)
def createReportData(self,data_sql_report):
self.data_sql_report = data_sql_report
self.df = pd.DataFrame(self.data_sql_report)
self.df.to_excel(self.docsend_Path,index=False)
#return False
#print(self.docsend_Path)
+44
View File
@@ -0,0 +1,44 @@
import paramiko
import os
class SFTPManager():
def __init__(self, sftp_host, sftp_port, sftp_username, sftp_password):
self.sftp_host = sftp_host
self.sftp_username = sftp_username
self.sftp_password = sftp_password
self.sftp_port = sftp_port or 22 # Puerto por defecto: 22
self.ssh_client = None
self.sftp_client = None
def _SFTPConect(self):
self.ssh_client = paramiko.SSHClient()
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # Acepta automáticamente la clave del host
self.ssh_client.connect(self.sftp_host, self.sftp_port, self.sftp_username, self.sftp_password)
self.sftp_client = self.ssh_client.open_sftp()
def _SFTPDisconnect(self):
if self.sftp_client:
self.sftp_client.close()
#print('SFTP desconectado.')
if self.ssh_client:
self.ssh_client.close()
#print('SSH desconectado.')
def uploadFile(self, task, sftp_path):
self._SFTPConect()
file_name = os.path.basename(task.docsend_Path)
pathFTP = os.path.join(sftp_path, file_name)
self.sftp_client.put(task.docsend_Path, pathFTP)
#print(f'Archivo {task.docsendPath} subido exitosamente a {pathFTP}')
self._SFTPDisconnect()
+109
View File
@@ -0,0 +1,109 @@
import requests
import os
import msal
from office365.sharepoint.client_context import ClientContext
from office365.runtime.auth.user_credential import UserCredential
from office365.runtime.auth.client_credential import ClientCredential
class SharepointManager:
def __init__(self, site, userSharepoint, passwordSharepoint):#,client_id,tenant_id,client_secret):
self.site = site
self.userSharepoint = userSharepoint
self.passwordSharepoint = passwordSharepoint
#self.client_id = client_id
#self.tenant_id = tenant_id
#self.client_secret = client_secret
#print(self.client_secret)
def uploadFile2(self, task, sharepoint_path):
self._connect2()
file_name = os.path.basename(task.docsend_Path)
with open(task.docsend_Path, "rb") as file_content:
folder = self.ctx.web.get_folder_by_server_relative_url(sharepoint_path)
upload = folder.upload_file(file_name, file_content.read()) # solo prepara la query
self.ctx.execute_query() # ejecuta en el contexto
print(f"✅ Archivo {file_name} subido a {upload.serverRelativeUrl}")
self._disconnect()
def _connect2(self):
#"""Conexión a SharePoint con access_token"""
#token = self._get_token()
#self.ctx = ClientContext(self.site).with_access_token(token)
#self.ctx.load(self.ctx.web)
#self.ctx.execute_query()
self.ctx = ClientContext(self.site).with_credentials(ClientCredential(self.client_id, self.client_secret))
self.ctx.load(self.ctx.web)
self.ctx.execute_query()
def _connect(self):
# 👇 Nada de AuthenticationContext aquí
self.ctx = ClientContext(self.site).with_credentials(
UserCredential(self.userSharepoint, self.passwordSharepoint)
)
self.ctx.load(self.ctx.web)
self.ctx.execute_query()
def _disconnect(self):
self.ctx = None
def uploadFile(self, task, sharepoint_path):
self._connect()
file_name = os.path.basename(task.docsend_Path)
with open(task.docsend_Path, "rb") as file_content:
response = (
self.ctx.web.get_folder_by_server_relative_url(sharepoint_path)
.upload_file(file_name, file_content.read())
.execute_query()
)
print(f"Archivo {file_name} subido a {response.serverRelativeUrl}")
self._disconnect()
def uploadFile2(self, task, sharepoint_path):
"""Subida con MSAL (App Registration)"""
self._connect2()
file_name = os.path.basename(task.docsend_Path)
with open(task.docsend_Path, "rb") as file_content:
folder = self.ctx.web.get_folder_by_server_relative_url(sharepoint_path)
upload = folder.upload_file(file_name, file_content.read())
self.ctx.execute_query()
print(f"✅ Archivo {file_name} subido a {upload.serverRelativeUrl}")
self._disconnect()
def uploadFileTemp(self, task, data):
self._connect()
file_name = os.path.basename(task.docsend_Path)
with open(task.docsend_Path, "rb") as file_content:
response = (
self.ctx.web.get_folder_by_server_relative_url(data[6])
.upload_file(file_name, file_content.read())
.execute_query()
)
print(f"Archivo {file_name} subido a {response.serverRelativeUrl}")
self._disconnect()
def dropFilesFolder(self, data):
self._connect()
folder = (
self.ctx.web.get_folder_by_server_relative_url(data[6])
.expand(["Files"])
.get()
.execute_query()
)
for file in folder.files:
print(f"Eliminando archivo: {file.name}")
file.delete_object()
self.ctx.execute_query()
self._disconnect()
+3
View File
@@ -0,0 +1,3 @@
from .Utils import *
from .Config import *
from .Services import *
View File
+200
View File
@@ -0,0 +1,200 @@
# API Reference
## Descripción General
DataTransferV2 incluye una API REST construida con Flask para consultar y descargar datos de reportes.
## Base URL
```
http://localhost:5000/api
```
## Endpoints
### GET /download
Descarga datos de un reporte específico en formato JSON.
#### Parámetros de Query
| Parámetro | Tipo | Requerido | Descripción |
|-----------|------|-----------|-------------|
| project | string | Sí | Nombre del proyecto (pg, feduro) |
| task | string | Sí | Nombre del reporte |
| start_date | string | Sí | Fecha inicio (YYYY-MM-DD) |
| end_date | string | Sí | Fecha fin (YYYY-MM-DD) |
#### Ejemplos de Uso
```bash
# Cobertura PG
curl "http://localhost:5000/api/download?project=pg&task=Cobertura&start_date=2024-01-01&end_date=2024-01-31"
# QualityDisplay FEDURO
curl "http://localhost:5000/api/download?project=feduro&task=QualityDisplay&start_date=2024-01-15&end_date=2024-01-15"
```
#### Respuesta Exitosa (200)
```json
{
"status": "success",
"project": "pg",
"task": "Cobertura",
"start_date": "2024-01-01",
"end_date": "2024-01-31",
"data": [
{
"fecha": "2024-01-01",
"tienda": "001",
"categoria": "A",
"ventas": 1500.50,
"unidades": 25
},
{
"fecha": "2024-01-02",
"tienda": "001",
"categoria": "B",
"ventas": 2100.75,
"unidades": 35
}
],
"count": 62,
"execution_time": "2.34s"
}
```
#### Respuestas de Error
##### 400 Bad Request - Parámetros Inválidos
```json
{
"status": "error",
"message": "Parámetros requeridos faltantes: project, task, start_date, end_date",
"code": 400
}
```
##### 404 Not Found - Proyecto/Reporte No Encontrado
```json
{
"status": "error",
"message": "Proyecto 'invalid' no encontrado. Proyectos disponibles: pg, feduro",
"code": 404
}
```
##### 500 Internal Server Error
```json
{
"status": "error",
"message": "Error de conexión a base de datos",
"code": 500
}
```
## Autenticación
Actualmente no requiere autenticación. Para producción, considera implementar:
- **Bearer Token:** `Authorization: Bearer <token>`
- **API Key:** `X-API-Key: <key>`
## Rate Limiting
No implementado actualmente. Recomendado para producción.
## Formatos de Datos
### Fechas
- Formato: `YYYY-MM-DD`
- Validación: Fechas válidas, start_date <= end_date
### Nombres de Proyecto
- `pg`: Pentagono Group
- `feduro`: Proyecto FEDURO
- `corripio`: Proyecto Corripio (deshabilitado)
### Nombres de Reportes
#### PG (16 reportes)
- Cobertura
- Negociadas
- Promociones
- QualityDisplay
- Checkout
- QualityShelf
- SOS
- OsaAnalogo
- OsaFotografico
- Fmot
- ModuloTareas
- NegociadasIS001
#### FEDURO (18 reportes)
- Cobertura
- Checkout
- OsaAnalogo
- OsaFotografico
- Promociones
- QualityDisplay
- QualityShelf
- SOS
- Fmot
- ModuloTareas
- NegociadasIS001
## Limitaciones
- **Máximo de registros:** No limitado (considera paginación)
- **Timeout:** 30 segundos por defecto
- **Formato de respuesta:** Solo JSON (considera CSV/XML)
## Ejemplos de Integración
### Python
```python
import requests
url = "http://localhost:5000/api/download"
params = {
"project": "pg",
"task": "Cobertura",
"start_date": "2024-01-01",
"end_date": "2024-01-31"
}
response = requests.get(url, params=params)
data = response.json()
if data["status"] == "success":
print(f"Obtenidos {data['count']} registros")
# Procesar data["data"]
```
### JavaScript
```javascript
fetch('http://localhost:5000/api/download?project=pg&task=Cobertura&start_date=2024-01-01&end_date=2024-01-31')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
console.log(`Datos obtenidos: ${data.count} registros`);
// Procesar data.data
}
});
```
## Monitoreo
Los requests a la API se registran en `Logs/api.log` con:
- Timestamp
- Endpoint
- Parámetros
- Tiempo de respuesta
- Código de estado
+81
View File
@@ -0,0 +1,81 @@
# Arquitectura
## Descripción General
DataTransferV2 sigue una arquitectura modular basada en el patrón ETL (Extract-Transform-Load), con separación clara de responsabilidades en capas.
## Tecnologías Principales
- **Backend:** Python 3.x con Flask
- **Base de Datos:** SQL Server (pyodbc)
- **Procesamiento de Datos:** Pandas, OpenPyXL
- **Distribución:** FTP (ftplib), SFTP (paramiko), SharePoint (office365), N8N webhooks
- **Notificaciones:** WhatsApp via webhooks
- **Programación:** APScheduler
- **Contenedorización:** Docker, Docker Compose
## Estructura de Directorios
```
DataTransferV2/
├── main.py # Punto de entrada principal
├── main2.py # Versión alternativa
├── App/
│ ├── Config/ # Configuraciones por proyecto
│ ├── Services/ # Servicios core (ETL, API, UI)
│ ├── Utils/ # Utilidades (DB, FTP, SFTP, etc.)
│ ├── Models/ # Modelos de datos (vacío actualmente)
│ ├── Static/ # Recursos estáticos para UI
│ └── Templates/ # Plantillas HTML para UI
├── requirements.txt # Dependencias Python
├── Dockerfile # Configuración Docker
├── docker-compose.yml # Orquestación de contenedores
├── Tests/ # Pruebas
├── Logs/ # Archivos de log
└── Reportes/ # Reportes generados
```
## Componentes Principales
### Capa de Configuración
- **GeneralConfig.py:** Configuraciones globales (DB, notificaciones, zonas horarias)
- **pgConfig.py:** Configuración específica para proyecto PG
- **feduroConfig.py:** Configuración para proyecto FEDURO
- **corripioConfig.py:** Plantilla para proyecto Corripio
### Capa de Servicios
- **etl_service.py:** Motor de orquestación ETL
- **api_service.py:** Endpoints REST API
- **ui_service.py:** Interfaz web Flask
### Capa de Utilidades
- **db_utils.py:** Gestor de conexiones y consultas SQL Server
- **report_utils.py:** Generador de reportes Excel
- **ftp_utils.py:** Cliente FTP con reintentos
- **sftp_utils.py:** Cliente SFTP para Azure
- **sharepoint_utils.py:** Integración con SharePoint
- **notification_utils.py:** Envío de notificaciones WhatsApp
## Patrón de Diseño
### Patrón Manager
Cada utilidad sigue el patrón Manager (DatabaseManager, FTPManager, etc.) para encapsular lógica específica.
### Patrón Pipeline
El flujo ETL sigue un pipeline claro: Extract → Transform → Load, con manejo de errores resiliente.
### Configuración Centralizada
Configuraciones separadas por proyecto permiten aislamiento y escalabilidad.
## Escalabilidad y Rendimiento
- **Procesamiento Paralelo:** Múltiples reportes procesados en secuencia
- **Conexiones Pooling:** Reutilización de conexiones DB
- **Reintentos Automáticos:** Para fallos en transferencias
- **Logging Estructurado:** Para monitoreo y debugging
## Seguridad
- Autenticación Bearer tokens
- Credenciales en configuración (considerar variables de entorno)
- Conexiones encriptadas (SFTP, HTTPS)
+248
View File
@@ -0,0 +1,248 @@
# Configuración
## Archivos de Configuración
La configuración está organizada en archivos separados por proyecto en `App/Config/`.
## GeneralConfig.py
Configuraciones globales aplicables a todos los proyectos.
### Base de Datos
```python
# SQL Server Connection
SERVER = "sql.glmanalytics.com"
DATABASES = {
"pg": {"id": "00080001", "name": "glm_pg"},
"feduro": {"id": "00010007", "name": "IS001"}
}
USERNAME = "tu_usuario"
PASSWORD = "tu_password"
```
### Zonas Horarias
```python
TIMEZONES = {
"central": "America/El_Salvador", # UTC-6
"caribbean": "America/Santo_Domingo" # UTC-4
}
```
### Notificaciones
```python
WHATSAPP_WEBHOOK = "https://automation.glmanalytics.com/webhook/wsp-notifcation"
EMAIL_CONFIG = {
"smtp_server": "smtp.gmail.com",
"port": 587,
"username": "tu_email@gmail.com",
"password": "tu_password"
}
```
### FTP Global
```python
FTP_CONFIG = {
"host": "rcomhost.com",
"username": "tu_usuario",
"password": "tu_password",
"base_path": "/www/TranferData_APP_PYTHON"
}
```
## Configuración por Proyecto
### pgConfig.py
Configuración específica para Pentagono Group.
```python
class PGConfig:
name = "pg"
timezone = "central"
reports = [
{"name": "Cobertura", "ftp": True, "sharepoint": True, "n8n": False},
{"name": "Checkout", "ftp": True, "sharepoint": False, "n8n": True},
# ... más reportes
]
sharepoint_config = {
"site_url": "https://pgone.sharepoint.com",
"client_id": "tu_client_id",
"tenant_id": "tu_tenant_id",
"folder_mappings": {
"Cobertura": "/sites/PGReports/Cobertura",
"Checkout": "/sites/PGReports/Checkout"
}
}
```
### feduroConfig.py
Configuración para proyecto FEDURO.
```python
class FeduroConfig:
name = "feduro"
timezone = "caribbean"
reports = [
{"name": "Cobertura", "ftp": False, "sftp": True, "n8n": False},
# ... más reportes
]
sftp_config = {
"host": "tu-azure-container-instance",
"username": "tu_usuario",
"password": "tu_password",
"port": 22,
"path_mappings": {
"Cobertura": "/feduro/reports/cobertura",
"Checkout": "/feduro/reports/checkout"
}
}
```
## Flags de Distribución
Cada reporte puede configurarse para múltiples destinos:
- **ftp:** Subida a servidor FTP
- **sftp:** Subida a servidor SFTP (Azure)
- **sharepoint:** Subida a SharePoint/OneDrive
- **n8n:** Trigger de webhook N8N
## Credenciales Seguras
### Variables de Entorno (Recomendado)
```bash
# Linux/macOS
export DB_USER="usuario_seguro"
export DB_PASSWORD="password_seguro"
export SHAREPOINT_CLIENT_ID="client_id_seguro"
# Windows
set DB_USER=usuario_seguro
set DB_PASSWORD=password_seguro
```
### Modificar Código para Usar Variables
```python
import os
USERNAME = os.getenv("DB_USER", "default_user")
PASSWORD = os.getenv("DB_PASSWORD", "default_password")
```
## Configuración de Docker
### Dockerfile
```dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "main.py"]
```
### docker-compose.yml
```yaml
version: '3.8'
services:
datatransfer:
build: .
ports:
- "5000:5000"
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
volumes:
- ./Reportes:/app/Reportes
- ./Logs:/app/Logs
```
## Validación de Configuración
### Verificar Conexiones
```python
# Probar conexión DB
from App.Utils.db_utils import DatabaseManager
db = DatabaseManager()
conn = db.get_connection("pg")
# Debería conectar sin errores
# Probar FTP
from App.Utils.ftp_utils import FTPManager
ftp = FTPManager()
ftp.connect()
# Debería conectar sin errores
```
### Validar Reportes
```python
# Verificar que todos los reportes tengan configuraciones válidas
for config in [PGConfig, FeduroConfig]:
for report in config.reports:
assert "name" in report
assert any([report.get("ftp"), report.get("sftp"), report.get("sharepoint"), report.get("n8n")])
```
## Configuración de Producción
### Variables de Entorno
- `FLASK_ENV=production`
- `SECRET_KEY=tu_clave_secreta`
- `LOG_LEVEL=INFO`
### Configuración de Logging
```python
LOGGING_CONFIG = {
"version": 1,
"formatters": {
"detailed": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"file": {
"class": "logging.FileHandler",
"filename": "Logs/app.log",
"formatter": "detailed"
}
},
"root": {
"level": "INFO",
"handlers": ["file"]
}
}
```
## Troubleshooting
### Problemas Comunes
1. **Conexión DB falla:** Verificar credenciales y firewall
2. **FTP timeout:** Revisar configuración de red y credenciales
3. **SharePoint error:** Validar client_id y permisos en Azure AD
4. **SFTP falla:** Confirmar host y puerto de Azure Container Instance
### Logs de Configuración
Los errores de configuración se registran en `Logs/etl.log` con mensajes descriptivos.
+307
View File
@@ -0,0 +1,307 @@
# Despliegue
## Opciones de Despliegue
DataTransferV2 puede desplegarse localmente, en contenedores Docker o en la nube.
## Despliegue Local
### Requisitos
- Python 3.8+
- SQL Server accesible
- Espacio en disco para reportes (~1GB/día)
### Pasos
1. **Instalar dependencias**
```bash
pip install -r requirements.txt
```
2. **Configurar entorno**
```bash
cp .env.example .env
# Editar .env con credenciales
```
3. **Ejecutar**
```bash
python main.py
```
4. **Programar ejecución**
```bash
# Usando cron (Linux)
0 */2 * * * /path/to/env/bin/python /path/to/main.py
# Usando Task Scheduler (Windows)
# Crear tarea programada cada 2 horas
```
## Despliegue con Docker
### Construir Imagen
```bash
# Desde directorio raíz
docker build -t datatransfer:latest .
```
### Ejecutar Contenedor
```bash
docker run -d \
--name datatransfer \
-p 5000:5000 \
-v $(pwd)/Reportes:/app/Reportes \
-v $(pwd)/Logs:/app/Logs \
-e DB_USER=$DB_USER \
-e DB_PASSWORD=$DB_PASSWORD \
datatransfer:latest
```
### Usando Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
datatransfer:
build: .
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- SHAREPOINT_CLIENT_ID=${SHAREPOINT_CLIENT_ID}
volumes:
- ./Reportes:/app/Reportes
- ./Logs:/app/Logs
restart: unless-stopped
```
```bash
docker-compose up -d
```
## Despliegue en la Nube
### Azure Container Instances
```bash
# Construir y subir imagen
az acr build --registry myregistry --image datatransfer:latest .
# Crear container instance
az container create \
--resource-group myResourceGroup \
--name datatransfer \
--image myregistry.azurecr.io/datatransfer:latest \
--cpu 1 --memory 1.5 \
--environment-variables \
DB_USER=$DB_USER \
DB_PASSWORD=$DB_PASSWORD \
--ports 5000 \
--restart-policy OnFailure
```
### Azure App Service
1. **Crear App Service**
```bash
az appservice plan create --name myPlan --resource-group myRG --sku B1
az webapp create --name datatransfer --plan myPlan --resource-group myRG
```
2. **Configurar deployment**
```bash
az webapp config appsettings set \
--name datatransfer \
--resource-group myRG \
--setting WEBSITES_PORT=5000
```
3. **Deploy con Git**
```bash
az webapp deployment source config-local-git \
--name datatransfer \
--resource-group myRG
```
### AWS EC2
```bash
# Instalar Docker en EC2
sudo yum update -y
sudo amazon-linux-extras install docker
sudo service docker start
sudo usermod -a -G docker ec2-user
# Ejecutar contenedor
docker run -d -p 5000:5000 \
-e DB_USER=$DB_USER \
-e DB_PASSWORD=$DB_PASSWORD \
datatransfer:latest
```
### Google Cloud Run
```bash
# Construir imagen
gcloud builds submit --tag gcr.io/PROJECT-ID/datatransfer
# Deploy
gcloud run deploy datatransfer \
--image gcr.io/PROJECT-ID/datatransfer \
--platform managed \
--port 5000 \
--set-env-vars DB_USER=$DB_USER,DB_PASSWORD=$DB_PASSWORD \
--allow-unauthenticated
```
## Configuración de Producción
### Variables de Entorno
```bash
# Base de datos
DB_USER=prod_user
DB_PASSWORD=prod_password
DB_SERVER=prod-sql-server.database.windows.net
# SharePoint
SHAREPOINT_CLIENT_ID=prod_client_id
SHAREPOINT_TENANT_ID=prod_tenant_id
# FTP
FTP_HOST=prod-ftp.example.com
FTP_USER=prod_ftp_user
FTP_PASSWORD=prod_ftp_password
# Aplicación
FLASK_ENV=production
SECRET_KEY=your-secret-key-here
LOG_LEVEL=INFO
```
### Configuración de Logging
```python
# En GeneralConfig.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'class': 'logging.FileHandler',
'filename': 'Logs/production.log',
'formatter': 'verbose',
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console', 'file'],
'level': 'INFO',
},
}
```
## Monitoreo y Mantenimiento
### Health Checks
```python
# Endpoint de health check
@app.route('/health')
def health():
# Verificar conexión DB
try:
db = DatabaseManager()
conn = db.get_connection('pg')
conn.close()
return {'status': 'healthy', 'database': 'ok'}
except Exception as e:
return {'status': 'unhealthy', 'database': str(e)}, 500
```
### Logs en la Nube
#### Azure Application Insights
```python
from applicationinsights import TelemetryClient
tc = TelemetryClient('your-instrumentation-key')
tc.track_event('ETL Started')
tc.track_metric('Reports Processed', report_count)
tc.flush()
```
#### AWS CloudWatch
```python
import boto3
cloudwatch = boto3.client('cloudwatch')
cloudwatch.put_metric_data(
Namespace='DataTransfer',
MetricData=[
{
'MetricName': 'ReportsProcessed',
'Value': report_count,
'Unit': 'Count'
}
]
)
```
### Backup y Recuperación
- **Reportes:** Los archivos Excel se generan diariamente
- **Logs:** Rotar logs semanalmente
- **Base de datos:** Backup automático de SQL Server
- **Configuración:** Versionar archivos de config en Git
## Escalado
### Horizontal Scaling
- **Múltiples instancias:** Ejecutar múltiples contenedores
- **Load balancer:** Distribuir requests entre instancias
- **Queue system:** Usar Redis/RabbitMQ para jobs ETL
### Vertical Scaling
- **CPU/Memoria:** Aumentar recursos del contenedor
- **Database:** Usar réplicas de lectura para consultas API
## Seguridad en Producción
### Red
- **Firewall:** Restringir acceso solo a IPs necesarias
- **VPC:** Aislar en red privada
- **HTTPS:** Usar certificados SSL
### Autenticación
- **API Keys:** Para acceso a API
- **OAuth:** Para UI web
- **Secrets Management:** Azure Key Vault, AWS Secrets Manager
### Actualizaciones
- **Zero-downtime:** Usar blue-green deployment
- **Rollback:** Mantener versiones anteriores
- **Testing:** Probar en staging antes de producción
+301
View File
@@ -0,0 +1,301 @@
# Flujo de Datos
## Pipeline ETL General
DataTransferV2 sigue un patrón ETL clásico con etapas bien definidas y manejo de errores resiliente.
```
EXTRACT → TRANSFORM → LOAD → NOTIFY
```
## Diagrama de Flujo
```
Inicio
Cargar Configuraciones (PG, FEDURO)
Para cada Proyecto:
┌─────────────────────────────────────┐
│ Para cada Reporte: │
│ Para cada Fecha (3 días): │
│ ┌─────────────────────────────┐ │
│ │ EJECUTAR ETL │ │
│ │ ┌─────────┐ ┌──────────┐ │ │
│ │ │ EXTRACT │→ │TRANSFORM│ │ │
│ │ └─────────┘ └──────────┘ │ │
│ │ ↓ │ │
│ │ ┌─────────┐ │ │
│ │ │ LOAD │ │ │
│ │ └─────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
Agregar Resultados
Enviar Notificación
Fin
```
## Etapa EXTRACT
### Proceso
1. **Conectar a Base de Datos**
- Usar pyodbc para conexión SQL Server
- Pooling de conexiones para eficiencia
2. **Construir Query**
```sql
SELECT * FROM {TableName} WHERE Fecha = '{YYYY-MM-DD}'
```
3. **Ejecutar Consulta**
- Timeout de 30 segundos
- Manejo de excepciones de conexión
4. **Validar Resultados**
- Contar registros obtenidos
- Verificar integridad de datos
### Estados Posibles
- **SUCCESS:** Datos encontrados (>0 registros)
- **WARNING:** Sin datos (=0 registros)
- **FAILED:** Error de conexión/query
## Etapa TRANSFORM
### Proceso
1. **Crear DataFrame**
- Usar pandas para manipulación de datos
- Validar tipos de datos
2. **Generar Excel**
- Usar openpyxl para formato Excel
- Crear hoja única con datos tabulares
- Aplicar formato básico (headers en negrita)
3. **Guardar Archivo**
```
Reportes/{PROJECT}/{REPORT}/{REPORT}-{YYYYMMDD}.xlsx
```
4. **Validar Archivo**
- Verificar que se creó correctamente
- Comprobar tamaño del archivo
### Formato de Salida
- **Nombre:** `{Reporte}-{Fecha}.xlsx`
- **Estructura:** Hoja única con headers
- **Encoding:** UTF-8
- **Separador:** Sin separador (datos crudos)
## Etapa LOAD
### Destinos Múltiples
Cada reporte puede distribuirse a múltiples destinos según flags de configuración.
#### FTP Upload
1. **Conectar al Servidor**
```python
ftp = FTP(host, user, password)
```
2. **Crear Directorios**
```
/TranferData_APP_PYTHON/{PROJECT}/{REPORT}/
```
3. **Subir Archivo**
- Batch de 100 archivos
- Reintentos automáticos en caso de fallo
4. **Verificar Upload**
- Comparar tamaños local/remoto
#### SFTP Upload (FEDURO)
1. **Conectar via SSH**
```python
ssh = paramiko.SSHClient()
ssh.connect(host, username, password)
sftp = ssh.open_sftp()
```
2. **Navegar a Directorio**
- Mapeo específico por reporte en `feduroConfig.py`
3. **Subir Archivo**
- Transferencia segura
- Verificación de integridad
#### SharePoint Upload (PG)
1. **Autenticar con Office365**
```python
ctx = ClientContext(site_url).with_credentials(credentials)
```
2. **Subir a Document Library**
- Mapeo de carpetas por reporte
- Sobrescribir archivos existentes
3. **Verificar Upload**
- Confirmar presencia en SharePoint
#### N8N Webhook
1. **Construir Payload**
```json
{
"project": "pg",
"report": "Cobertura",
"date": "2024-01-15",
"file_path": "/path/to/file.xlsx"
}
```
2. **Enviar POST Request**
- Trigger automatización N8N
- No espera respuesta
### Estrategia de Distribución
- **Paralelo:** Todos los destinos se procesan simultáneamente
- **Resiliente:** Fallo en un destino no afecta otros
- **Logging:** Cada upload se registra individualmente
## Agregación de Resultados
### Reglas de Negocio
1. **Éxito (SUCCESS)**
- Datos encontrados, Excel creado, todos los uploads exitosos
2. **Completado con Errores (COMPLETED_WITH_ERRORS)**
- Datos procesados, pero algunos uploads fallaron
- No bloquea el pipeline
3. **Advertencia (WARNING)**
- Sin datos para la fecha
- Ignorado si es domingo
4. **Fallo (FAILED)**
- Error en consulta DB o creación de Excel
- Detiene procesamiento del reporte
### Lógica de Consolidación
```python
# Reglas de negocio para warnings
if consecutive_warnings >= 3:
status = "FAILED"
elif is_sunday and no_data:
status = "IGNORED"
elif no_data:
status = "WARNING"
else:
status = "SUCCESS"
```
## Notificaciones
### WhatsApp Summary
Después de completar cada proyecto:
```
✅ PG - Completado
Total: 16, Éxito: 15, Errores: 1, Advertencias: 0
Tiempo de ejecución: 45 minutos
Detalles de errores: Cobertura-2024-01-15 (FTP timeout)
```
### Contenido del Mensaje
- **Estado del proyecto:** ✅ ⚠️ ❌
- **Estadísticas:** Total, éxito, errores, advertencias
- **Tiempo:** Duración total
- **Errores específicos:** Lista de reportes/fechas con problemas
## Manejo de Errores
### Tipos de Error
1. **Errores de Conexión**
- DB: Reintentar conexión
- FTP/SFTP: Reintentar upload
- SharePoint: Reautenticar
2. **Errores de Datos**
- Sin datos: Advertencia (no error)
- Datos corruptos: Fallo crítico
3. **Errores de Sistema**
- Disco lleno: Fallo crítico
- Memoria insuficiente: Reinicio automático
### Recuperación
- **Reintentos:** Hasta 3 intentos para operaciones de red
- **Fallback:** Continuar con otros destinos si uno falla
- **Logging:** Todos los errores se registran con contexto completo
## Rendimiento
### Métricas Típicas
- **Tiempo por reporte:** 30-120 segundos
- **Throughput:** 16-18 reportes por proyecto (~30-60 min)
- **Uso de memoria:** 100-500MB por ejecución
- **I/O:** 10-50MB por reporte Excel
### Optimizaciones
- **Conexiones pool:** Reutilización de conexiones DB
- **Procesamiento por lotes:** Múltiples fechas en una ejecución
- **Compresión:** Archivos Excel optimizados
- **Paralelización:** Uploads simultáneos
## Auditoría
### Logging a Base de Datos
Cada ejecución se registra en `ExecutionLog`:
```sql
INSERT INTO ExecutionLog (
ProcedureID, Name, Status, Message,
StartTime, EndTime, Duration
) VALUES (
1001, 'Cobertura', 'SUCCESS', 'Processed 1500 records',
'2024-01-15 08:00:00', '2024-01-15 08:02:30', 150
)
```
### Logs de Aplicación
- `Logs/etl.log`: Progreso detallado
- `Logs/api.log`: Requests API
- `Logs/reports.log`: Generación de reportes
## Monitoreo
### KPIs Principales
- **Tasa de éxito:** % de reportes procesados correctamente
- **Tiempo promedio:** Duración por reporte/proyecto
- **Errores por tipo:** DB, red, sistema
- **Volumen de datos:** Registros/filas procesadas
### Alertas
- Error rate > 10%
- Duración > 2x promedio
- Fallos consecutivos en mismo reporte
+21
View File
@@ -0,0 +1,21 @@
# Índice de Documentación
Bienvenido a la documentación de **DataTransferV2**, una aplicación ETL automatizada para transferencias de datos.
## Archivos de Documentación
- **[Arquitectura.md](Arquitectura.md)**: Descripción de la arquitectura general, componentes y tecnologías utilizadas.
- **[Instalacion.md](Instalacion.md)**: Guía paso a paso para instalar y configurar el proyecto.
- **[Uso.md](Uso.md)**: Instrucciones para usar la aplicación, incluyendo ejecución automática, interfaz web y API.
- **[API.md](API.md)**: Referencia completa de la API REST para descargas de datos.
- **[Configuracion.md](Configuracion.md)**: Detalles sobre la configuración de proyectos, bases de datos y destinos.
- **[Despliegue.md](Despliegue.md)**: Guías para desplegar la aplicación en producción usando Docker.
- **[Flujo_de_Datos.md](Flujo_de_Datos.md)**: Explicación del pipeline ETL y flujo de datos.
- **[Manejo_de_Errores.md](Manejo_de_Errores.md)**: Información sobre clasificación de errores, reglas de negocio y logging.
- **[Seguridad.md](Seguridad.md)**: Notas sobre seguridad, credenciales y mejores prácticas.
## Acerca del Proyecto
DataTransferV2 es un sistema ETL que extrae datos de SQL Server, genera reportes Excel y los distribuye a múltiples plataformas. Incluye UI web y API para operaciones manuales.
Para más información, consulta el [README.md](../README.md) en la raíz del proyecto.
+110
View File
@@ -0,0 +1,110 @@
# Instalación
## Prerrequisitos
- **Python:** Versión 3.8 o superior
- **Base de Datos:** SQL Server con bases de datos configuradas (glm_pg, IS001)
- **Sistema Operativo:** Windows, Linux o macOS
- **Docker:** Opcional, para contenedorización
## Instalación Local
### 1. Clonar el Repositorio
```bash
git clone <url-del-repositorio>
cd DataTransferV2
```
### 2. Crear Entorno Virtual
```bash
# Windows
python -m venv env
env\Scripts\activate
# Linux/macOS
python3 -m venv env
source env/bin/activate
```
### 3. Instalar Dependencias
```bash
pip install -r requirements.txt
```
### 4. Configurar la Base de Datos
Asegúrate de que SQL Server esté ejecutándose y accesible. Las bases de datos requeridas:
- `glm_pg` (ID: 00080001)
- `IS001` (ID: 00010007)
- Servidor: sql.glmanalytics.com
### 5. Configurar Credenciales
Edita `App/Config/GeneralConfig.py` con:
- Credenciales de SQL Server
- Endpoints FTP/SFTP/SharePoint
- URLs de webhooks para notificaciones
### 6. Verificar Instalación
```bash
python main.py --help # Si tiene opción help
```
## Instalación con Docker
### Construir Imagen
```bash
docker build -t datatransfer .
```
### Ejecutar Contenedor
```bash
docker run -p 5000:5000 datatransfer
```
### Usando Docker Compose
```bash
docker-compose up -d
```
## Configuración de Entorno
### Variables de Entorno (Recomendado)
Para mayor seguridad, usa variables de entorno en lugar de hardcodear credenciales:
```bash
export DB_USER="tu_usuario"
export DB_PASSWORD="tu_password"
export SHAREPOINT_CLIENT_ID="tu_client_id"
```
### Configuración de Zonas Horarias
El sistema maneja zonas horarias de Centroamérica y el Caribe. Asegúrate de que el servidor esté en la zona correcta.
## Solución de Problemas
### Error de Conexión DB
- Verifica credenciales en GeneralConfig.py
- Confirma que SQL Server esté ejecutándose
- Revisa firewall y permisos de red
### Dependencias Faltantes
- Asegúrate de tener pip actualizado: `pip install --upgrade pip`
- Instala dependencias específicas: `pip install pyodbc azure-storage-blob`
### Problemas con SharePoint
- Verifica client_id y tenant_id
- Confirma permisos de aplicación en Azure AD
## Próximos Pasos
Después de la instalación, configura los proyectos específicos y ejecuta una prueba con `python main.py`.
+372
View File
@@ -0,0 +1,372 @@
# Manejo de Errores
## Clasificación de Errores
DataTransferV2 clasifica los errores en tres categorías principales con diferentes impactos en el pipeline.
## Estados de Ejecución
### SUCCESS (✅)
**Definición:** Ejecución completamente exitosa
**Condiciones:**
- Datos encontrados en base de datos
- Archivo Excel generado correctamente
- Todos los destinos de distribución exitosos
**Ejemplo:**
```
✅ Cobertura-2024-01-15: 1,500 registros procesados, FTP+SharePoint OK
```
### COMPLETED_WITH_ERRORS (⚠️)
**Definición:** Datos procesados pero con fallos en distribución
**Condiciones:**
- Datos encontrados y Excel generado
- Al menos un destino de distribución falló
- Otros destinos pueden haber sido exitosos
**Impacto:** No detiene el pipeline, continúa con siguientes reportes
**Ejemplo:**
```
⚠️ Checkout-2024-01-15: Datos OK, FTP falló (timeout), SharePoint OK
```
### WARNING (️)
**Definición:** Sin datos disponibles
**Condiciones:**
- Query ejecutada correctamente
- 0 registros retornados
- No se genera archivo Excel
**Reglas de negocio:**
- Si es domingo + sin datos → Ignorado
- 3+ warnings consecutivos → Se eleva a FAILED
**Ejemplo:**
```
️ Promociones-2024-01-15: Sin datos disponibles
```
### FAILED (❌)
**Definición:** Fallo crítico que detiene el procesamiento
**Condiciones:**
- Error en conexión a base de datos
- Error en query SQL
- Error en generación de Excel
- 3+ warnings consecutivos en mismo reporte
**Impacto:** Detiene procesamiento del reporte actual
**Ejemplo:**
```
❌ QualityDisplay-2024-01-15: Error de conexión DB - timeout
```
## Tipos de Errores Técnicos
### Errores de Base de Datos
#### Connection Timeout
```
Error: [08001] [Microsoft][ODBC SQL Server Driver]Timeout expired
Causa: Red lenta, servidor sobrecargado
Solución: Reintentar, verificar conectividad
```
#### Authentication Failed
```
Error: [28000] [Microsoft][ODBC SQL Server Driver]Login failed
Causa: Credenciales incorrectas
Solución: Verificar USERNAME/PASSWORD en config
```
#### Database Not Found
```
Error: [08004] [Microsoft][ODBC SQL Server Driver]Server does not exist
Causa: Servidor incorrecto o no accesible
Solución: Verificar SERVER en config
```
#### Query Syntax Error
```
Error: [42000] [Microsoft][ODBC SQL Server Driver]Incorrect syntax
Causa: Nombre de tabla/columna incorrecto
Solución: Verificar configuración de reportes
```
### Errores de FTP
#### Connection Failed
```
Error: [Errno 110] Connection timed out
Causa: Firewall, red bloqueada
Solución: Verificar host/puerto, permisos de red
```
#### Authentication Failed
```
Error: 530 Login incorrect
Causa: Credenciales FTP incorrectas
Solución: Verificar FTP_CONFIG
```
#### Permission Denied
```
Error: 550 Permission denied
Causa: Usuario sin permisos de escritura
Solución: Verificar permisos en servidor FTP
```
#### Disk Full
```
Error: 452 Insufficient storage space
Causa: Espacio insuficiente en servidor
Solución: Liberar espacio, contactar administrador
```
### Errores de SFTP
#### SSH Connection Failed
```
Error: [Errno 61] Connection refused
Causa: Puerto cerrado, servidor no responde
Solución: Verificar host/puerto SFTP
```
#### Host Key Verification
```
Error: Host key verification failed
Causa: Primera conexión, clave host no conocida
Solución: Agregar host a known_hosts o usar policy
```
#### File Transfer Error
```
Error: Permission denied (publickey,password)
Causa: Autenticación fallida
Solución: Verificar credenciales SFTP
```
### Errores de SharePoint
#### Authentication Failed
```
Error: Invalid client credentials
Causa: client_id/tenant_id incorrectos
Solución: Verificar configuración Azure AD
```
#### Permission Denied
```
Error: Access denied
Causa: Usuario sin permisos en site/library
Solución: Verificar permisos en SharePoint
```
#### File Upload Failed
```
Error: The file exists
Causa: Archivo ya existe, no se puede sobrescribir
Solución: Verificar configuración de sobrescritura
```
### Errores de Sistema
#### Disk Full
```
Error: [Errno 28] No space left on device
Causa: Disco local lleno
Solución: Liberar espacio, verificar partición
```
#### Memory Error
```
Error: MemoryError
Causa: DataFrame muy grande
Solución: Optimizar query, procesar en lotes
```
#### Encoding Error
```
Error: 'utf-8' codec can't decode byte
Causa: Datos con caracteres especiales
Solución: Especificar encoding correcto
```
## Estrategias de Recuperación
### Reintentos Automáticos
```python
def retry_operation(operation, max_retries=3, delay=5):
for attempt in range(max_retries):
try:
return operation()
except Exception as e:
if attempt < max_retries - 1:
time.sleep(delay * (attempt + 1)) # Backoff exponencial
else:
raise e
```
**Aplicado a:**
- Conexiones de red (FTP, SFTP, SharePoint)
- Queries de base de datos
- Uploads de archivos
### Fallback Strategies
1. **Múltiples Destinos:** Si FTP falla, intentar SFTP
2. **Modo Degradado:** Continuar sin notificaciones si webhook falla
3. **Cache Local:** Guardar archivos localmente si uploads fallan
### Circuit Breaker
```python
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=300):
self.failure_count = 0
self.last_failure_time = None
self.state = 'CLOSED' # CLOSED, OPEN, HALF_OPEN
def call(self, func):
if self.state == 'OPEN':
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = 'HALF_OPEN'
else:
raise CircuitBreakerError("Circuit is OPEN")
try:
result = func()
self._on_success()
return result
except Exception as e:
self._on_failure()
raise e
```
## Logging y Monitoreo
### Niveles de Log
- **DEBUG:** Detalles técnicos, queries ejecutadas
- **INFO:** Progreso normal, estadísticas
- **WARNING:** Situaciones no críticas
- **ERROR:** Errores que requieren atención
- **CRITICAL:** Errores que detienen el sistema
### Formato de Logs
```
2024-01-15 08:30:15,123 - ETL - INFO - Starting ETL for PG project
2024-01-15 08:30:16,456 - DB - DEBUG - Executing query: SELECT * FROM Cobertura WHERE Fecha = '2024-01-15'
2024-01-15 08:30:18,789 - REPORT - INFO - Generated Excel: Reportes/PG/Cobertura/Cobertura-20240115.xlsx
2024-01-15 08:30:20,012 - FTP - ERROR - Upload failed: Connection timeout
2024-01-15 08:30:25,345 - FTP - INFO - Retry 1/3 successful
```
### Base de Datos de Auditoría
Cada error se registra en `ExecutionLog`:
```sql
INSERT INTO ExecutionLog (
ProcedureID, StartTime, EndTime, Status,
ErrorType, ErrorMessage, RetryCount
) VALUES (
1001, '2024-01-15 08:30:15', '2024-01-15 08:30:25', 'COMPLETED_WITH_ERRORS',
'FTP_TIMEOUT', 'Connection timed out after 30s', 1
)
```
## Alertas y Notificaciones
### Umbrales de Alerta
- **Error Rate > 10%:** Notificación inmediata
- **3+ fallos consecutivos:** Alerta crítica
- **Tiempo de ejecución > 2x promedio:** Warning
- **Disco > 90% usado:** Alerta de capacidad
### Canales de Notificación
1. **WhatsApp:** Resúmenes diarios
2. **Email:** Alertas críticas
3. **Logs:** Registro detallado
4. **Dashboard:** Métricas en tiempo real (futuro)
## Debugging
### Herramientas de Diagnóstico
#### Verificar Conectividad
```bash
# Base de datos
sqlcmd -S sql.glmanalytics.com -U $USERNAME -P $PASSWORD -Q "SELECT 1"
# FTP
ftp $FTP_HOST
# SFTP
sftp $SFTP_USER@$SFTP_HOST
```
#### Analizar Logs
```bash
# Últimos errores
tail -f Logs/etl.log | grep ERROR
# Errores por tipo
grep "ERROR" Logs/etl.log | cut -d' ' -f4 | sort | uniq -c
```
#### Probar Componentes Individualmente
```python
# Probar DB
from App.Utils.db_utils import DatabaseManager
db = DatabaseManager()
data = db.execute_query("SELECT TOP 1 * FROM Cobertura")
# Probar FTP
from App.Utils.ftp_utils import FTPManager
ftp = FTPManager()
ftp.test_connection()
```
### Checklist de Troubleshooting
- [ ] Credenciales correctas en config
- [ ] Servidores accesibles (ping, telnet)
- [ ] Puertos abiertos (1433 DB, 21 FTP, 22 SFTP, 443 HTTPS)
- [ ] Espacio en disco suficiente
- [ ] Permisos de archivo correctos
- [ ] Dependencias Python instaladas
- [ ] Zona horaria correcta
- [ ] Configuración de reportes válida
## Mejores Prácticas
### Prevención
1. **Validación de Configuración:** Verificar al inicio
2. **Health Checks:** Monitorear componentes críticos
3. **Backup:** Mantener copias de configuraciones
4. **Versionado:** Control de cambios en config
### Respuesta
1. **Documentar Incidentes:** Registrar causa y resolución
2. **Automatizar Recuperación:** Scripts para reintentos
3. **Escalada:** Definir cuándo notificar a equipo
4. **Post-mortem:** Análisis después de incidentes críticos
### Mejora Continua
1. **Métricas:** Recopilar estadísticas de errores
2. **Análisis de Tendencias:** Identificar patrones
3. **Optimización:** Reducir puntos de fallo
4. **Testing:** Cobertura de escenarios de error
+356
View File
@@ -0,0 +1,356 @@
# Seguridad
## Principios de Seguridad
DataTransferV2 maneja datos sensibles y requiere medidas de seguridad apropiadas para entornos de producción.
## Gestión de Credenciales
### Problema Actual
**Las credenciales están hardcodeadas en archivos de configuración:**
```python
# App/Config/GeneralConfig.py
USERNAME = "usuario_db"
PASSWORD = "password_db"
SHAREPOINT_CLIENT_ID = "client_id"
```
### Soluciones Recomendadas
#### 1. Variables de Entorno
```bash
# Archivo .env
DB_USER=usuario_seguro
DB_PASSWORD=password_seguro
SHAREPOINT_CLIENT_ID=client_id_seguro
FTP_PASSWORD=password_ftp_seguro
```
```python
# En GeneralConfig.py
import os
USERNAME = os.getenv("DB_USER")
PASSWORD = os.getenv("DB_PASSWORD")
SHAREPOINT_CLIENT_ID = os.getenv("SHAREPOINT_CLIENT_ID")
```
#### 2. Secrets Management
##### Azure Key Vault
```python
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
client = SecretClient(vault_url="https://myvault.vault.azure.net/", credential=credential)
DB_PASSWORD = client.get_secret("db-password").value
```
##### AWS Secrets Manager
```python
import boto3
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='prod/datatransfer/db')
credentials = json.loads(response['SecretString'])
DB_USER = credentials['username']
DB_PASSWORD = credentials['password']
```
##### HashiCorp Vault
```python
import hvac
client = hvac.Client(url='https://vault.example.com:8200')
client.auth.approle.login(role_id='role_id', secret_id='secret_id')
DB_PASSWORD = client.read('secret/datatransfer/db')['data']['password']
```
## Autenticación y Autorización
### API Security
#### Implementar API Keys
```python
from flask import request, abort
API_KEYS = os.getenv("API_KEYS", "").split(",")
def require_api_key(func):
@wraps(func)
def decorated(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key not in API_KEYS:
abort(401, "Invalid API key")
return func(*args, **kwargs)
return decorated
@app.route('/api/download')
@require_api_key
def download_data():
# Endpoint protegido
```
#### JWT Tokens
```python
from flask_jwt_extended import JWTManager, jwt_required
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')
jwt = JWTManager(app)
@app.route('/api/download')
@jwt_required()
def download_data():
# Requiere token JWT válido
```
### UI Web Security
#### Autenticación de Usuario
```python
from flask_login import LoginManager, login_required
login_manager = LoginManager()
login_manager.init_app(app)
@app.route('/etl')
@login_required
def etl_interface():
# Solo usuarios autenticados
```
#### Rate Limiting
```python
from flask_limiter import Limiter
limiter = Limiter(app, key_func=lambda: request.remote_addr)
@app.route('/api/download')
@limiter.limit("10 per minute")
def download_data():
# Máximo 10 requests por minuto por IP
```
## Encriptación de Datos
### En Tránsito
#### HTTPS para API
```python
from flask_sslify import SSLify
sslify = SSLify(app) # Fuerza HTTPS
```
#### Conexiones Encriptadas
- **Base de Datos:** Usar `encrypt=yes` en connection string
- **FTP:** Considerar FTPS (FTP over SSL)
- **SFTP:** Ya encriptado por defecto
- **SharePoint:** HTTPS obligatorio
### En Reposo
#### Encriptar Archivos Excel
```python
from openpyxl import Workbook
from openpyxl.styles import Protection
wb = Workbook()
ws = Protection.set_password('password_seguro')
wb.security = Protection(workbookPassword='password_seguro')
```
#### Encriptar Base de Datos
Configurar SQL Server con Transparent Data Encryption (TDE).
## Control de Acceso
### Principio de Menor Privilegio
#### Usuario de Base de Datos
```sql
-- Crear usuario con permisos mínimos
CREATE USER datatransfer_user WITH PASSWORD = 'password';
GRANT SELECT ON Cobertura TO datatransfer_user;
GRANT SELECT ON Checkout TO datatransfer_user;
-- Solo permisos de lectura
```
#### Usuario FTP
- Solo permisos de escritura en directorio específico
- No permisos de lectura/eliminación
#### Usuario SharePoint
- Acceso solo a document libraries específicas
- Permisos de Contributor (no Owner)
### Network Security
#### Firewall Rules
```bash
# Solo permitir conexiones desde IPs específicas
ufw allow from 192.168.1.0/24 to any port 5000
ufw allow from 10.0.0.0/8 to any port 5000
ufw deny from 0.0.0.0/0 to any port 5000
```
#### VPC/Subnets
- Desplegar en red privada
- Usar NAT Gateway para acceso outbound
- Security Groups restrictivos
## Auditoría y Monitoreo
### Logging Seguro
```python
# No loggear credenciales
logger.info(f"Connected to database: {db_config['server']}")
# ❌ MAL: logger.info(f"Connected with user: {USERNAME}")
# Sanitizar datos sensibles en logs
def sanitize_log(message):
# Remover passwords, tokens, etc.
return re.sub(r'password=\w+', 'password=***', message)
```
### Monitoreo de Seguridad
#### Detección de Intrusiones
- Monitorear logs para patrones sospechosos
- Alertas en accesos desde IPs desconocidas
- Rate limiting para prevenir ataques de fuerza bruta
#### Vulnerability Scanning
```bash
# Escanear dependencias
safety check
pip-audit
# Escanear imagen Docker
docker scan datatransfer:latest
```
## Backup y Recuperación
### Estrategia de Backup
#### Configuración
```bash
# Backup de configs
tar -czf config_backup_$(date +%Y%m%d).tar.gz App/Config/
# Versionado en Git (sin secrets)
git add App/Config/
git commit -m "Config update"
```
#### Datos
- **Reportes:** Backup automático en storage redundante
- **Logs:** Rotación y compresión semanal
- **Base de Datos:** Backup SQL Server programado
### Plan de Recuperación
#### Disaster Recovery
1. **Restaurar desde backup**
2. **Reconfigurar credenciales**
3. **Verificar integridad**
4. **Reanudar operaciones**
#### Business Continuity
- **RTO (Recovery Time Objective):** 4 horas
- **RPO (Recovery Point Objective):** 1 hora
- **Multi-region:** Despliegue en múltiples zonas
## Cumplimiento
### GDPR (Europa)
- **Derecho al olvido:** Capacidad de eliminar datos
- **Consentimiento:** Documentar uso de datos
- **Auditoría:** Logs de acceso a datos personales
### SOX (EEUU)
- **Controles internos:** Validación de procesos ETL
- **Auditoría financiera:** Traceability de datos
- **Integridad:** Checks de validación de datos
### ISO 27001
- **Asset Management:** Inventario de datos sensibles
- **Access Control:** Autenticación multifactor
- **Incident Management:** Procedimientos de respuesta
## Mejores Prácticas
### Desarrollo Seguro
1. **Code Reviews:** Revisar código por seguridad
2. **Dependency Scanning:** Verificar vulnerabilidades en paquetes
3. **Input Validation:** Sanitizar todas las entradas
4. **Error Handling:** No exponer información sensible en errores
### Despliegue Seguro
1. **Imágenes Base:** Usar imágenes oficiales y actualizadas
2. **Secrets:** Nunca en código o imágenes
3. **Network:** Principio de zero trust
4. **Monitoring:** Alertas en tiempo real
### Operaciones Seguras
1. **Access Management:** Rotación regular de credenciales
2. **Backup Testing:** Verificar restauración periódicamente
3. **Incident Response:** Plan documentado y probado
4. **Training:** Capacitación en seguridad para equipo
## Checklist de Seguridad
### Antes de Producción
- [ ] Credenciales en variables de entorno
- [ ] HTTPS configurado
- [ ] Autenticación implementada
- [ ] Rate limiting activo
- [ ] Logs sanitizados
- [ ] Backup funcionando
- [ ] Firewall configurado
- [ ] Dependencias actualizadas
### Monitoreo Continuo
- [ ] Alertas de seguridad activas
- [ ] Logs revisados regularmente
- [ ] Vulnerabilidades escaneadas
- [ ] Accesos auditados
- [ ] Backups verificados
+188
View File
@@ -0,0 +1,188 @@
# Uso
## Modos de Ejecución
DataTransferV2 puede ejecutarse de tres formas principales: automática, interfaz web y API.
## Ejecución Automática
### Comando Principal
```bash
python main.py
```
Este comando procesa todos los proyectos activos (PG, FEDURO) con todos sus reportes en una ventana de 3 días (hoy, ayer, anteayer).
### Salida Esperada
- Registros de progreso en consola
- Archivos Excel generados en `Reportes/<PROYECTO>/<REPORTE>/`
- Notificación WhatsApp con resumen de ejecución
- Logs detallados en `Logs/etl.log`
### Ejemplo de Salida
```
Iniciando ETL para proyecto: PG
Procesando reporte: Cobertura - Fecha: 2024-01-15
✅ SUCCESS: Reporte generado y distribuido
...
Resumen PG: Total: 16, Éxito: 15, Errores: 1, Advertencias: 0
```
## Interfaz Web
### Iniciar la UI
```bash
cd App
python -m flask run
```
Accede a `http://localhost:5000` en tu navegador.
### Funcionalidades
- **Selección de Proyecto:** PG, FEDURO, Corripio
- **Selección de Reporte:** Lista desplegable con reportes disponibles
- **Rango de Fechas:** Selector de fecha inicio/fin
- **Ejecución Manual:** Botón para procesar la selección
### Uso
1. Selecciona proyecto y reporte
2. Elige fechas de inicio y fin
3. Haz clic en "Ejecutar ETL"
4. Monitorea el progreso y resultados
## API REST
### Endpoint Principal
```
GET /api/download
```
### Parámetros
- `project` (requerido): Nombre del proyecto (pg, feduro)
- `task` (requerido): Nombre del reporte
- `start_date` (requerido): Fecha inicio (YYYY-MM-DD)
- `end_date` (requerido): Fecha fin (YYYY-MM-DD)
### Ejemplo de Uso
```bash
curl "http://localhost:5000/api/download?project=pg&task=Cobertura&start_date=2024-01-01&end_date=2024-01-31"
```
### Respuesta
```json
{
"status": "success",
"data": [
{
"fecha": "2024-01-01",
"campo1": "valor1",
"campo2": "valor2"
}
],
"count": 100
}
```
### Códigos de Estado
- `200`: Éxito
- `400`: Parámetros inválidos
- `404`: Proyecto/reporte no encontrado
- `500`: Error interno
## Programación Automática
### Usando APScheduler
El código incluye APScheduler para ejecución programada. Para activarlo:
```python
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
scheduler.add_job(run_etl_pipeline, 'interval', hours=2)
scheduler.start()
```
### Usando Cron (Linux/macOS)
```bash
# Ejecutar cada 2 horas
0 */2 * * * /path/to/env/bin/python /path/to/main.py
```
### Usando Task Scheduler (Windows)
1. Crear tarea básica
2. Programa: `python.exe`
3. Argumentos: `main.py`
4. Ejecutar cada 2 horas
## Monitoreo y Logs
### Archivos de Log
- `Logs/etl.log`: Logs del proceso ETL
- `Logs/api.log`: Logs de la API
- `Logs/reports.log`: Logs de generación de reportes
### Base de Datos de Auditoría
Todas las ejecuciones se registran en la tabla `ExecutionLog` de SQL Server con:
- ID de procedimiento
- Estado (SUCCESS, FAILED, WARNING)
- Mensajes de error
- Timestamps
## Reportes Generados
### Ubicación
```
Reportes/
├── FEDURO/
│ ├── Cobertura/
│ │ ├── Cobertura-20240115.xlsx
│ │ └── ...
│ └── ...
└── PG/
├── Checkout/
│ ├── Checkout-20240115.xlsx
│ └── ...
└── ...
```
### Formato
- **Tipo:** Excel (.xlsx)
- **Librería:** Pandas + OpenPyXL
- **Contenido:** Datos tabulares de consultas SQL
## Notificaciones
### WhatsApp
Después de cada proyecto, se envía un resumen via webhook:
```
✅ PG - Completado
Total: 16, Éxito: 15, Errores: 1
Tiempo: 45 minutos
```
### Estados
- ✅ SUCCESS: Todo correcto
- ⚠️ COMPLETED_WITH_ERRORS: Errores no críticos
- ❌ FAILED: Fallo crítico
- ️ WARNING: Sin datos (ignorados si domingo)
+130
View File
@@ -0,0 +1,130 @@
# DataTransferV2
## Descripción
DataTransferV2 es una aplicación automatizada de ETL (Extract-Transform-Load) desarrollada en Python que extrae datos de bases de datos SQL Server, los transforma en reportes Excel y los distribuye a través de múltiples plataformas como FTP, SFTP, SharePoint y webhooks N8N. El sistema se ejecuta de manera programada y incluye una interfaz web para ejecución manual de ETL y una API para descargas de datos.
La aplicación está diseñada para automatizar transferencias de datos de inteligencia de negocio para clientes minoristas, procesando reportes comerciales (Cobertura, Negociadas, Promociones, QualityDisplay, Checkout, etc.) en una ventana móvil de 3 días.
**Versión:** 2.0.0
**Autor:** eurbina
## Características Principales
- **Soporte Multi-Proyecto:** Maneja proyectos independientes como PG (Pentagono Group), FEDURO y Corripio con configuraciones aisladas.
- **Procesamiento Multi-Reporte:** Procesa 16-18 reportes por proyecto automáticamente.
- **Procesamiento por Lotes:** Ejecuta una ventana móvil de 3 días (hoy, ayer, anteayer).
- **Múltiples Distribuciones:** FTP, SFTP, SharePoint, webhooks N8N y notificaciones WhatsApp.
- **Manejo de Errores:** Errores no bloqueantes permiten que el pipeline continúe; errores catastróficos se registran.
- **Lógica de Reintento:** Reintentos automáticos para subidas FTP fallidas.
- **Registro de Auditoría:** Todas las ejecuciones se registran en la base de datos con timestamps, estados y mensajes.
- **Soporte de Zonas Horarias:** Manejo explícito para zonas horarias de Centroamérica y el Caribe.
- **Interfaz Web:** Interfaz para disparadores manuales de ETL con selección de proyecto/reporte/fechas.
- **API REST:** Endpoint para descargas de datos con parámetros de consulta.
- **Contenedorizado:** Configuración Docker y Docker Compose para despliegue.
## Tecnologías Utilizadas
- **Lenguaje:** Python 3.x
- **Framework Web:** Flask
- **Base de Datos:** SQL Server (via pyodbc)
- **Formatos de Archivo:** Excel (openpyxl, pandas)
- **Integración en la Nube:** SharePoint (office365), Azure Storage Blob
- **Transferencia de Archivos:** FTP, SFTP (paramiko), webhooks N8N
- **Notificaciones:** WhatsApp (via webhooks)
- **Programación:** APScheduler (programación de tareas en segundo plano)
- **Autenticación:** Tokens Bearer, credenciales de usuario
- **Pruebas:** pytest
- **Contenedorización:** Docker & Docker Compose
## Arquitectura y Componentes
### Estructura del Proyecto
- **main.py:** Punto de entrada principal para ejecución programada de ETL.
- **main2.py:** Versión alternativa/prototipo.
- **App/Config/:** Configuraciones por proyecto (GeneralConfig, pgConfig, feduroConfig, etc.).
- **App/Services/:** Servicios core (etl_service, api_service, ui_service).
- **App/Utils/:** Utilidades (db_utils, report_utils, ftp_utils, sftp_utils, sharepoint_utils, notification_utils).
- **App/Models/:** Modelos de base de datos (actualmente vacío).
- **App/Static/ y App/Templates/:** Recursos estáticos y plantillas para la UI web.
- **requirements.txt:** Dependencias Python.
- **Dockerfile y docker-compose.yml:** Configuración para contenedorización.
- **Tests/ y tests/:** Pruebas unitarias.
- **Logs/:** Archivos de log.
- **Reportes/:** Reportes generados organizados por proyecto y tipo.
### Flujo de Datos
1. **Inicialización:** Carga configuraciones de proyectos.
2. **Para cada proyecto:** Itera sobre reportes y fechas (3 días).
3. **ETL por reporte:**
- **Extract:** Consulta SQL Server.
- **Transform:** Genera archivo Excel.
- **Load:** Distribuye según flags de configuración (FTP, SFTP, SharePoint, N8N).
4. **Agregación de Resultados:** Cuenta éxitos, fallos, errores y advertencias.
5. **Notificaciones:** Envía resumen por WhatsApp.
## Instalación y Configuración
### Prerrequisitos
- Python 3.x
- SQL Server con bases de datos configuradas
- Credenciales para FTP, SFTP, SharePoint
- Docker (opcional para contenedorización)
### Instalación
1. Clona el repositorio.
2. Crea un entorno virtual: `python -m venv env`
3. Activa el entorno: `env\Scripts\activate` (Windows)
4. Instala dependencias: `pip install -r requirements.txt`
### Configuración
Edita los archivos en `App/Config/` para configurar:
- Credenciales de base de datos
- Endpoints de FTP/SFTP/SharePoint
- Webhooks de notificaciones
## Uso
### Ejecución Automática
Ejecuta `python main.py` para procesar todos los proyectos y reportes en la ventana de 3 días.
### Interfaz Web
Lanza la UI: `python -m flask run` (desde App/) y accede a `http://localhost:5000` para ejecución manual.
### API
Endpoint: `GET /api/download?project=<>&task=<>&start_date=<>&end_date=<>`
Devuelve datos en JSON/CSV.
### Docker
```bash
docker build -t datatransfer .
docker run -p 5000:5000 datatransfer
```
## Estado Actual
- **Proyectos Activos:** PG, FEDURO (Corripio comentado)
- **Reportes:** ~16-18 por proyecto
- **Procesamiento Diario:** ~102 ejecuciones ETL por ciclo
## Notas de Seguridad
- Las credenciales están hardcodeadas en GeneralConfig.py (considera usar variables de entorno).
- Implementa autenticación adecuada para producción.
## Contribuciones
Si deseas contribuir, por favor contacta al autor o crea un issue en el repositorio.
## Licencia
[Especifica la licencia si aplica]
+13
View File
@@ -0,0 +1,13 @@
# docker-compose.yml (opcional, si necesitas DB externa)
version: '3.8'
services:
app:
build: .
ports:
- "5000:5000"
volumes:
- ./Logs:/app/Logs
- ./Reportes:/app/Reportes
environment:
- FLASK_ENV=development
# Si tienes DB externa, agrega aquí (ej. sqlserver service)
+127
View File
@@ -0,0 +1,127 @@
#from App import *
#from requests import *
from datetime import datetime, timedelta
from collections import defaultdict
from App.Config.feduroConfig import FeduroConfig
from App.Services.etl_service import *
from App.Utils.notification_utils import send_whatsapp, build_message
#Iniciando variables para resumen y mensaje final
# Cargando configuración del proyecto
configs = [
# FeduroConfig(),
PGConfig(),
# CorripioConfig(),
]
#Iniciando proceso ETL para cada proyecto por reporte y cada fecha (últimos 3 días)
for config in configs:
summary = {
"total": 0,
"success": 0,
"failed": [], # catastróficos
"errors": [], # no bloqueantes
"warnings": []
}
warning_counter = defaultdict(int)
warning_details = defaultdict(list)
start_time = datetime.now()
print(f"\n🚀 Ejecutando proyecto: {config.project_name}")
for report in config.reports:
data_report = {
'project_name': config.project_name,
'report_name': report['ReportName'],
'Table_Name': report['TableName'],
'reportID': report['ReportID'],
'FTP': report['FTP'],
'SFTP': report['SFTP'],
'SharePoint': report['SharePoint'],
'N8N': report['N8N']
}
for i in range(3):
target_date_report = datetime.now() - timedelta(days=i)
print(f"Ejecutando ETL para {data_report['report_name']} - Fecha: {target_date_report.strftime('%Y-%m-%d')}")
result = run_etl(config, data_report, target_date_report)
summary["total"] += 1
if result["status"] == "SUCCESS":
summary["success"] += 1
elif result["status"] == "FAILED":
summary["failed"].append(f"{result['report']} ({target_date_report.strftime('%Y-%m-%d')}) → {result['message']}")
elif result["status"] == "COMPLETED_WITH_ERRORS":
summary["errors"].append(
f"{result['report']} ({target_date_report.strftime('%Y-%m-%d')}) → {result['message']}"
)
elif result["status"] == "WARNING":
is_sunday = target_date_report.weekday() == 6
is_no_data = "Sin datos" in result["message"]
# Ignorar solo domingos SIN datos
if is_sunday and is_no_data:
continue
key = result["report"]
warning_counter[key] += 1
warning_details[key].append({
"date": target_date_report.strftime('%Y-%m-%d'),
"message": result["message"]
})
for report, count in warning_counter.items():
messages = warning_details[report]
# Tomamos el mensaje base (todos son iguales normalmente)
base_message = messages[0]["message"]
if count >= 3:
summary["failed"].append(
f"{report}{base_message} ({count} días consecutivos)"
)
else:
for m in messages:
summary["warnings"].append(
f"{report} ({m['date']}) → {m['message']}"
)
end_time = datetime.now()
duration = end_time - start_time
message = build_message(config.project_name, summary, duration)
print(message) # debug
send_whatsapp(message)
print("end")
+152
View File
@@ -0,0 +1,152 @@
from App import *
from requests import *
pg_config = PGConfig()
database = DatabaseManager(config.database, config.databaseID)
run_etl(config, database, data_report)
pg_config = PGConfig()
projects = {'PG':pg_config,
#'Feduro':feduroConfig,
#'Cimberton':cimbertonConfig}
}
print(projects)
for project, config in projects.items():
print(f"Iniciando servicio ETL para el proyecto {project} y {config}")
report_list = config.reportsID
run_etl(project, config, report_list=report_list)
import time
import importlib
import pyodbc
from datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler
# 🔌 conexión a SQL Server
CONN_STR = """
DRIVER={ODBC Driver 18 for SQL Server};
SERVER=sql.glmanalytics.com;
DATABASE=glm_pg;
UID=vmglm001;
PWD=tu_password;
Encrypt=yes;
TrustServerCertificate=yes;
"""
# 📥 cargar configuración desde BD
def get_clients_config():
conn = pyodbc.connect(CONN_STR)
cursor = conn.cursor()
cursor.execute("""
SELECT DISTINCT client_name, frequency_minutes
FROM etl_report_config
WHERE is_active = 1
""")
rows = cursor.fetchall()
conn.close()
configs = []
for row in rows:
configs.append({
"client": row[0],
"frequency": row[1]
})
return configs
# 📦 cargar config desde carpeta /config
def load_client_config(client_name):
try:
module = importlib.import_module(f"config.{client_name}")
return module.CONFIG
except Exception as e:
raise Exception(f"No se pudo cargar config de {client_name}: {e}")
# ⚙️ lógica ETL por cliente
def run_etl(client_name):
try:
print(f"\n[{datetime.now()}] 🚀 Iniciando ETL para {client_name}")
config = load_client_config(client_name)
# 🔥 aquí usas tu config
connection = config.get("connection")
tables = config.get("tables", [])
print(f"Tablas a procesar: {tables}")
# 👉 ejemplo de flujo
for table in tables:
print(f"Procesando {client_name} - {table}")
# extract()
# transform()
# load()
print(f"[{datetime.now()}] ✅ ETL finalizado {client_name}")
except Exception as e:
print(f"[ERROR] ❌ {client_name}: {e}")
# 🧠 job que ejecuta el scheduler
def job(client_name):
run_etl(client_name)
# 🚀 iniciar scheduler
def start_scheduler():
scheduler = BackgroundScheduler()
configs = get_clients_config()
for config in configs:
client = config["client"]
frequency = config["frequency"]
scheduler.add_job(
job,
'interval',
minutes=frequency,
args=[client],
id=f"job_{client}",
replace_existing=True
)
print(f"✅ Job registrado: {client} cada {frequency} minutos")
scheduler.start()
print("\n🚀 Scheduler iniciado correctamente\n")
# mantener vivo (IMPORTANTE en Docker)
try:
while True:
time.sleep(60)
except (KeyboardInterrupt, SystemExit):
scheduler.shutdown()
# ▶️ entrypoint
if __name__ == "__main__":
start_scheduler()
+9
View File
@@ -0,0 +1,9 @@
# Dependencias para el proyecto ETL v2
Flask==2.3.3
pandas==2.0.3
openpyxl==3.1.2
pyodbc==4.0.39 # Para conexión a SQL Server (ajusta si usas otra DB)
APScheduler==3.10.4 # Para scheduler cada 2 horas
office365==0.3.1 # Para SharePoint (ajusta si usas otra lib)
pytest==7.4.0 # Para tests
# Agrega más según necesites (ej. sqlalchemy si usas ORM)
+115
View File
@@ -0,0 +1,115 @@
import requests
import json
from datetime import datetime
# ---------------------------
# CONFIG
# ---------------------------
OLLAMA_URL = "http://136.115.239.38:11434/api/generate"
MODEL = "qwen:14b"
pg_config = PGConfig()
database = DatabaseManager(pg_config.database, pg_config.databaseID)
# ---------------------------
# 1. TU QUERY (ya la tienes)
# ---------------------------
query = f"""
SELECT TOP (100)-- [ID]
-- ,[FieldID]
[ExecutionID]
,[ProcedureID]
,[ProcedureName]
,[ProcedureType]
,[TargetDate]
,[Status]
,[ExecutedBy]
,[MessageDescription]
,[Source]
,[ExtraInfo]
,[ExecutionDate]
FROM [glm_pg].[dbo].[ExecutionLog]
where [Status]='FAILED' AND [TargetDate]>'2026-04-01'
ORDER BY [ExecutionDate] DESC
"""
print("Query ejecutada:")
print(query)
# ---------------------------
# 2. EJECUTAR QUERY
# ---------------------------
data_sql_report = database.get_execute_query(query)
# ---------------------------
# 3. FORMATEAR DATOS
# ---------------------------
def format_data(data):
try:
return json.dumps(data, indent=2, default=str)
except:
return str(data)
data_formatted = format_data(data_sql_report)
# ---------------------------
# 4. CREAR PROMPT
# ---------------------------
prompt = f"""
Eres un analista de datos experto.
Tengo los siguientes datos obtenidos desde SQL:
QUERY:
{query}
RESULTADO:
{data_formatted}
TAREA:
1. Analiza los datos
2. Identifica patrones importantes
3. Detecta posibles errores o anomalías
4. Da recomendaciones claras
Responde de forma clara y estructurada.
"""
# ---------------------------
# 5. GUARDAR PROMPT EN TXT
# ---------------------------
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_name = f"prompt_{timestamp}.txt"
with open(file_name, "w", encoding="utf-8") as f:
f.write(prompt)
print(f"\nPrompt guardado en: {file_name}")
# ---------------------------
# 6. (OPCIONAL) ENVIAR A IA
# ---------------------------
send_to_ai = True # cambia a False si solo quieres el TXT
print(datetime.now())
if send_to_ai:
response = requests.post(
OLLAMA_URL,
json={
"model": MODEL,
"prompt": prompt,
"stream": False
}
)
result = response.json()
print("\nRespuesta IA:\n")
print(result["response"])
print(datetime.now())
print(datetime.now())
+332
View File
@@ -0,0 +1,332 @@
# Tests para App/Utils/db_utils.py
import pytest
from unittest.mock import Mock, MagicMock, patch, call
from App.Utils.db_utils import DatabaseManager, get_db_connection
class TestDatabaseManager:
"""Tests para la clase DatabaseManager"""
@patch('App.Utils.db_utils.ConfigDatabase')
def test_init(self, mock_config):
"""Test que DatabaseManager se inicializa correctamente con credenciales"""
# Arrange
mock_config.db_user = 'test_user'
mock_config.db_password = 'test_pass'
mock_config.db_host = 'test_host'
# Act
manager = DatabaseManager('test_db', 'test_id')
# Assert
assert manager.db_database == 'test_db'
assert manager.db_databaseID == 'test_id'
assert manager.db_user == 'test_user'
assert manager.db_password == 'test_pass'
assert manager.db_host == 'test_host'
@patch('App.Utils.db_utils.pyodbc.connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_connect_success(self, mock_config, mock_pyodbc_connect):
"""Test que _connect establece conexión exitosa"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
mock_connection = MagicMock()
mock_pyodbc_connect.return_value = mock_connection
manager = DatabaseManager('test_db', 'test_id')
# Act
manager._connect()
# Assert
assert manager.connection == mock_connection
mock_pyodbc_connect.assert_called_once()
assert 'ODBC Driver 17 for SQL Server' in manager.conn_str
@patch('App.Utils.db_utils.pyodbc.connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_connect_failure(self, mock_config, mock_pyodbc_connect):
"""Test que _connect maneja errores de conexión"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
mock_pyodbc_connect.side_effect = Exception("Connection failed")
manager = DatabaseManager('test_db', 'test_id')
# Act
manager._connect()
# Assert
assert manager.connection is None
@patch('App.Utils.db_utils.ConfigDatabase')
def test_disconnect(self, mock_config):
"""Test que _disconnect cierra la conexión"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = MagicMock()
# Act
manager._disconnect()
# Assert
manager.connection.close.assert_called_once()
@patch('App.Utils.db_utils.ConfigDatabase')
def test_disconnect_no_connection(self, mock_config):
"""Test que _disconnect no falla si no hay conexión"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = None
# Act & Assert (no debe lanzar excepción)
manager._disconnect()
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_get_execute_query_success(self, mock_config, mock_connect, mock_disconnect):
"""Test que get_execute_query devuelve datos como lista de dicts"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = MagicMock()
manager.cursor = MagicMock()
# Simula resultado de query
manager.cursor.description = [('id',), ('name',)]
manager.cursor.fetchall.return_value = [(1, 'Alice'), (2, 'Bob')]
manager.connection.cursor.return_value = manager.cursor
# Act
results, error = manager.get_execute_query("SELECT * FROM users")
# Assert
assert error is None
assert len(results) == 2
assert results[0] == {'id': 1, 'name': 'Alice'}
assert results[1] == {'id': 2, 'name': 'Bob'}
manager.cursor.execute.assert_called_once_with("SELECT * FROM users")
mock_disconnect.assert_called_once()
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_get_execute_query_no_connection(self, mock_config, mock_connect, mock_disconnect):
"""Test que get_execute_query devuelve error si no hay conexión"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = None
# Act
results, error = manager.get_execute_query("SELECT * FROM users")
# Assert
assert results is None
assert error == "Error de conexión"
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_get_execute_query_error(self, mock_config, mock_connect, mock_disconnect):
"""Test que get_execute_query maneja errores de query"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = MagicMock()
manager.cursor = MagicMock()
manager.cursor.execute.side_effect = Exception("invalid column")
manager.connection.cursor.return_value = manager.cursor
# Act
results, error = manager.get_execute_query("SELECT invalid_col FROM users")
# Assert
assert results is None
assert "invalid column" in error
mock_disconnect.assert_called_once()
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_set_execute_query_success(self, mock_config, mock_connect, mock_disconnect):
"""Test que set_execute_query hace commit correctamente"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = MagicMock()
cursor_mock = MagicMock()
manager.connection.cursor.return_value = cursor_mock
# Act
result = manager.set_execute_query("INSERT INTO users VALUES (1, 'Alice')")
# Assert
assert result is True
cursor_mock.execute.assert_called_once_with("INSERT INTO users VALUES (1, 'Alice')")
manager.connection.commit.assert_called_once()
cursor_mock.close.assert_called_once()
mock_disconnect.assert_called_once()
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_set_execute_query_no_connection(self, mock_config, mock_connect, mock_disconnect):
"""Test que set_execute_query devuelve False si no hay conexión"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = None
# Act
result = manager.set_execute_query("INSERT INTO users VALUES (1, 'Alice')")
# Assert
assert result is False
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_set_execute_query_error(self, mock_config, mock_connect, mock_disconnect):
"""Test que set_execute_query maneja errores"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = MagicMock()
cursor_mock = MagicMock()
cursor_mock.execute.side_effect = Exception("Constraint violation")
manager.connection.cursor.return_value = cursor_mock
# Act
result = manager.set_execute_query("INSERT INTO users VALUES (1, 'Alice')")
# Assert
assert result is False
mock_disconnect.assert_called_once()
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_log_execution_success(self, mock_config, mock_connect, mock_disconnect):
"""Test que log_execution ejecuta el SP correctamente"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = MagicMock()
manager.cursor = MagicMock()
manager.connection.cursor.return_value = manager.cursor
# Act
manager.log_execution(
procedure_id=1,
procedure_name='test_proc',
status='Success',
message='Test completed',
executed_by='test_user',
extra_info='None',
target_date='2023-01-01',
date='2023-01-01'
)
# Assert
assert manager.cursor.execute.called
manager.connection.commit.assert_called_once()
mock_disconnect.assert_called_once()
@patch.object(DatabaseManager, '_disconnect')
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_log_execution_no_connection(self, mock_config, mock_connect, mock_disconnect):
"""Test que log_execution maneja falta de conexión"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
manager = DatabaseManager('test_db', 'test_id')
manager.connection = None
# Act & Assert (no debe lanzar excepción)
manager.log_execution(
procedure_id=1,
procedure_name='test_proc',
status='Error',
message='Connection failed',
executed_by='test_user',
extra_info='None',
target_date='2023-01-01',
date='2023-01-01'
)
class TestGetDbConnection:
"""Tests para la función get_db_connection"""
@patch.object(DatabaseManager, '_connect')
@patch('App.Utils.db_utils.ConfigDatabase')
def test_get_db_connection_returns_connection(self, mock_config, mock_connect):
"""Test que get_db_connection devuelve una conexión pyodbc"""
# Arrange
mock_config.db_user = 'user'
mock_config.db_password = 'pass'
mock_config.db_host = 'server'
mock_connection = MagicMock()
with patch.object(DatabaseManager, '__init__', return_value=None):
with patch.object(DatabaseManager, '_connect'):
manager = DatabaseManager.__new__(DatabaseManager)
manager.connection = mock_connection
# Act (simulamos el comportamiento)
conn = manager.connection
# Assert
assert conn == mock_connection
@patch('App.Utils.db_utils.DatabaseManager')
def test_get_db_connection_params(self, mock_db_manager_class):
"""Test que get_db_connection pasa parámetros correctamente"""
# Arrange
mock_instance = MagicMock()
mock_instance.connection = MagicMock()
mock_db_manager_class.return_value = mock_instance
# Act
conn = get_db_connection(database='custom_db', databaseID='custom_id')
# Assert
mock_db_manager_class.assert_called_once_with('custom_db', 'custom_id')
mock_instance._connect.assert_called_once()
+19
View File
@@ -0,0 +1,19 @@
import requests
url = "https://automation.glmanalytics.com/webhook/new-task-clickup"
payload = {
"id": "12345",
"title": "Prueba ticket desde Python",
"description": "Este es un ticket de prueba enviado manualmente",
"priority": "3",
"status": "Nuevo",
"requester": "Juan Perez",
"category": "Soporte",
"created_at": "2026-04-09 10:00:00"
}
response = requests.post(url, json=payload)
print(response.status_code)
print(response.text)