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