commit e0ec11977c520d6846e5bc858809986311ed40fa Author: Eidan T. Date: Sat May 9 14:00:04 2026 -0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aeb35d9 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/App/Config/GeneralConfig.py b/App/Config/GeneralConfig.py new file mode 100644 index 0000000..8a2e21f --- /dev/null +++ b/App/Config/GeneralConfig.py @@ -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 \ No newline at end of file diff --git a/App/Config/__init__.py b/App/Config/__init__.py new file mode 100644 index 0000000..d291b16 --- /dev/null +++ b/App/Config/__init__.py @@ -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 * diff --git a/App/Config/corripioConfig.py b/App/Config/corripioConfig.py new file mode 100644 index 0000000..5b375ef --- /dev/null +++ b/App/Config/corripioConfig.py @@ -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' + } + + + diff --git a/App/Config/feduroConfig.py b/App/Config/feduroConfig.py new file mode 100644 index 0000000..088af98 --- /dev/null +++ b/App/Config/feduroConfig.py @@ -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, + } + + + + \ No newline at end of file diff --git a/App/Config/pgConfig.py b/App/Config/pgConfig.py new file mode 100644 index 0000000..5dfb8be --- /dev/null +++ b/App/Config/pgConfig.py @@ -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' + } + diff --git a/App/Config/version.py b/App/Config/version.py new file mode 100644 index 0000000..75a6c5a --- /dev/null +++ b/App/Config/version.py @@ -0,0 +1,3 @@ +APP_NAME = "PYTHON_TRANSFERDATA_APP" +VERSION = "2.0.0" +AUTHOR = "eurbina" \ No newline at end of file diff --git a/App/Models/database_models.py b/App/Models/database_models.py new file mode 100644 index 0000000..3a8c7e7 --- /dev/null +++ b/App/Models/database_models.py @@ -0,0 +1,2 @@ +# Modelos de datos (ej. esquemas de tablas si usas ORM) +# Por ahora vacío; agrega clases si usas SQLAlchemy \ No newline at end of file diff --git a/App/Services/__init__.py b/App/Services/__init__.py new file mode 100644 index 0000000..e32f572 --- /dev/null +++ b/App/Services/__init__.py @@ -0,0 +1 @@ +#from .etl_service import * diff --git a/App/Services/api_service.py b/App/Services/api_service.py new file mode 100644 index 0000000..be52a63 --- /dev/null +++ b/App/Services/api_service.py @@ -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 \ No newline at end of file diff --git a/App/Services/etl_service.py b/App/Services/etl_service.py new file mode 100644 index 0000000..2d45884 --- /dev/null +++ b/App/Services/etl_service.py @@ -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')""" \ No newline at end of file diff --git a/App/Services/ui_service.py b/App/Services/ui_service.py new file mode 100644 index 0000000..ec5b8c6 --- /dev/null +++ b/App/Services/ui_service.py @@ -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') \ No newline at end of file diff --git a/App/Static/styles.css b/App/Static/styles.css new file mode 100644 index 0000000..8861e5b --- /dev/null +++ b/App/Static/styles.css @@ -0,0 +1,3 @@ +/* Estilos básicos para UI */ +body { font-family: Arial; } +form { max-width: 400px; } \ No newline at end of file diff --git a/App/Templates/index.html b/App/Templates/index.html new file mode 100644 index 0000000..69cc786 --- /dev/null +++ b/App/Templates/index.html @@ -0,0 +1,14 @@ + + +DataTransfer UI + +

Actualizar Datos

+
+ Proyecto:
+ Tarea:
+ Fecha inicio:
+ Fecha fin:
+ +
+ + \ No newline at end of file diff --git a/App/Templates/query create.sql b/App/Templates/query create.sql new file mode 100644 index 0000000..7243e9e --- /dev/null +++ b/App/Templates/query create.sql @@ -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 \ No newline at end of file diff --git a/App/Utils/__init__.py b/App/Utils/__init__.py new file mode 100644 index 0000000..a365af2 --- /dev/null +++ b/App/Utils/__init__.py @@ -0,0 +1,5 @@ +#from .db_utils import * +#from .report_utils import * +#from .sharepoint_utils import * +#from .ftp_utils import * +#from .sftp_utils import * \ No newline at end of file diff --git a/App/Utils/db_utils.py b/App/Utils/db_utils.py new file mode 100644 index 0000000..53cd1ed --- /dev/null +++ b/App/Utils/db_utils.py @@ -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. diff --git a/App/Utils/ftp_utils.py b/App/Utils/ftp_utils.py new file mode 100644 index 0000000..46047b2 --- /dev/null +++ b/App/Utils/ftp_utils.py @@ -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() diff --git a/App/Utils/notification_utils.py b/App/Utils/notification_utils.py new file mode 100644 index 0000000..7c18ebf --- /dev/null +++ b/App/Utils/notification_utils.py @@ -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 diff --git a/App/Utils/report_utils.py b/App/Utils/report_utils.py new file mode 100644 index 0000000..21d571a --- /dev/null +++ b/App/Utils/report_utils.py @@ -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) \ No newline at end of file diff --git a/App/Utils/sftp_utils.py b/App/Utils/sftp_utils.py new file mode 100644 index 0000000..7cbc1ed --- /dev/null +++ b/App/Utils/sftp_utils.py @@ -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() diff --git a/App/Utils/sharepoint_utils.py b/App/Utils/sharepoint_utils.py new file mode 100644 index 0000000..0fafa5f --- /dev/null +++ b/App/Utils/sharepoint_utils.py @@ -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() + + diff --git a/App/__init__.py b/App/__init__.py new file mode 100644 index 0000000..868fbaa --- /dev/null +++ b/App/__init__.py @@ -0,0 +1,3 @@ +from .Utils import * +from .Config import * +from .Services import * \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/Documentacion/API.md b/Documentacion/API.md new file mode 100644 index 0000000..6c8ed9e --- /dev/null +++ b/Documentacion/API.md @@ -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 ` +- **API Key:** `X-API-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 \ No newline at end of file diff --git a/Documentacion/Arquitectura.md b/Documentacion/Arquitectura.md new file mode 100644 index 0000000..5f42160 --- /dev/null +++ b/Documentacion/Arquitectura.md @@ -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) \ No newline at end of file diff --git a/Documentacion/Configuracion.md b/Documentacion/Configuracion.md new file mode 100644 index 0000000..406dc58 --- /dev/null +++ b/Documentacion/Configuracion.md @@ -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. \ No newline at end of file diff --git a/Documentacion/Despliegue.md b/Documentacion/Despliegue.md new file mode 100644 index 0000000..640df2a --- /dev/null +++ b/Documentacion/Despliegue.md @@ -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 \ No newline at end of file diff --git a/Documentacion/Flujo_de_Datos.md b/Documentacion/Flujo_de_Datos.md new file mode 100644 index 0000000..4d16023 --- /dev/null +++ b/Documentacion/Flujo_de_Datos.md @@ -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 \ No newline at end of file diff --git a/Documentacion/Indice.md b/Documentacion/Indice.md new file mode 100644 index 0000000..b65f3a7 --- /dev/null +++ b/Documentacion/Indice.md @@ -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. \ No newline at end of file diff --git a/Documentacion/Instalacion.md b/Documentacion/Instalacion.md new file mode 100644 index 0000000..2bbfdfd --- /dev/null +++ b/Documentacion/Instalacion.md @@ -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 +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`. \ No newline at end of file diff --git a/Documentacion/Manejo_de_Errores.md b/Documentacion/Manejo_de_Errores.md new file mode 100644 index 0000000..7e6a3b5 --- /dev/null +++ b/Documentacion/Manejo_de_Errores.md @@ -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 \ No newline at end of file diff --git a/Documentacion/Seguridad.md b/Documentacion/Seguridad.md new file mode 100644 index 0000000..508d21d --- /dev/null +++ b/Documentacion/Seguridad.md @@ -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 \ No newline at end of file diff --git a/Documentacion/Uso.md b/Documentacion/Uso.md new file mode 100644 index 0000000..e829902 --- /dev/null +++ b/Documentacion/Uso.md @@ -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///` +- 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) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c0553d --- /dev/null +++ b/README.md @@ -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] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3353996 --- /dev/null +++ b/docker-compose.yml @@ -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) diff --git a/main.py b/main.py new file mode 100644 index 0000000..bf50e3d --- /dev/null +++ b/main.py @@ -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") + + + + + + diff --git a/main2.py b/main2.py new file mode 100644 index 0000000..398295c --- /dev/null +++ b/main2.py @@ -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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9f4ed14 --- /dev/null +++ b/requirements.txt @@ -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) \ No newline at end of file diff --git a/tests/IA.py b/tests/IA.py new file mode 100644 index 0000000..386f0fc --- /dev/null +++ b/tests/IA.py @@ -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()) + + diff --git a/tests/test_db_utils.py b/tests/test_db_utils.py new file mode 100644 index 0000000..027737a --- /dev/null +++ b/tests/test_db_utils.py @@ -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() diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..f07e706 --- /dev/null +++ b/tests/test_utils.py @@ -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) \ No newline at end of file