first commit
This commit is contained in:
+183
@@ -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/
|
||||
@@ -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
|
||||
@@ -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 *
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
APP_NAME = "PYTHON_TRANSFERDATA_APP"
|
||||
VERSION = "2.0.0"
|
||||
AUTHOR = "eurbina"
|
||||
@@ -0,0 +1,2 @@
|
||||
# Modelos de datos (ej. esquemas de tablas si usas ORM)
|
||||
# Por ahora vacío; agrega clases si usas SQLAlchemy
|
||||
@@ -0,0 +1 @@
|
||||
#from .etl_service import *
|
||||
@@ -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
|
||||
@@ -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')"""
|
||||
@@ -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')
|
||||
@@ -0,0 +1,3 @@
|
||||
/* Estilos básicos para UI */
|
||||
body { font-family: Arial; }
|
||||
form { max-width: 400px; }
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
#from .db_utils import *
|
||||
#from .report_utils import *
|
||||
#from .sharepoint_utils import *
|
||||
#from .ftp_utils import *
|
||||
#from .sftp_utils import *
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .Utils import *
|
||||
from .Config import *
|
||||
from .Services import *
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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())
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user