motulco 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of motulco might be problematic. Click here for more details.
- motulco/__init__.py +8 -0
- motulco/colors_utils.py +12 -0
- motulco/df_utils.py +185 -0
- motulco/doc_utils.py +84 -0
- motulco/excel_utils.py +126 -0
- motulco/pdf_utils.py +29 -0
- motulco/ppt_utils.py +309 -0
- motulco/py_utils.py +54 -0
- motulco/shapes_utils.py +409 -0
- motulco-0.2.0.dist-info/METADATA +23 -0
- motulco-0.2.0.dist-info/RECORD +14 -0
- motulco-0.2.0.dist-info/WHEEL +5 -0
- motulco-0.2.0.dist-info/licenses/LICENSE +1 -0
- motulco-0.2.0.dist-info/top_level.txt +1 -0
motulco/__init__.py
ADDED
motulco/colors_utils.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
################################################################################################################################
|
|
5
|
+
################################################################################################################################
|
|
6
|
+
####################################################### Convierte de HEX a RGB #################################################
|
|
7
|
+
################################################################################################################################
|
|
8
|
+
################################################################################################################################
|
|
9
|
+
def hex_to_rgb(hex_color):
|
|
10
|
+
"""Convierte color hex (#RRGGBB) a tupla RGB (R,G,B)"""
|
|
11
|
+
hex_color = hex_color.lstrip("#")
|
|
12
|
+
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
motulco/df_utils.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import os
|
|
3
|
+
from unidecode import unidecode
|
|
4
|
+
from openpyxl import load_workbook
|
|
5
|
+
|
|
6
|
+
################################################################################################################################
|
|
7
|
+
################################################################################################################################
|
|
8
|
+
################################# Quita los .0 en los textos de un df en las columnas dadas ####################################
|
|
9
|
+
################################################################################################################################
|
|
10
|
+
################################################################################################################################
|
|
11
|
+
def QuitarPuntosDecimalesTextos(df, columnas):
|
|
12
|
+
"""Convierte las columnas especificadas a string y elimina el sufijo '.0' de los valores numéricos.
|
|
13
|
+
Returns:
|
|
14
|
+
pd.DataFrame: El DataFrame con las columnas modificadas.
|
|
15
|
+
"""
|
|
16
|
+
df[columnas] = df[columnas].astype(str).replace(r'^(\d+)\.0$', r'\1', regex=True)
|
|
17
|
+
return df
|
|
18
|
+
|
|
19
|
+
################################################################################################################################
|
|
20
|
+
################################################################################################################################
|
|
21
|
+
################################## Hace la union (merge/join) de df1 y df2 y valida cruces ####################################
|
|
22
|
+
################################################################################################################################
|
|
23
|
+
################################################################################################################################
|
|
24
|
+
def mergeBases(dataframe1, dataframe2, lefton, righton):
|
|
25
|
+
"""Une dos DataFrames de Pandas asegurando que no haya duplicados en el cruce y detectando registros sin coincidencias en cada DataFrame.
|
|
26
|
+
|
|
27
|
+
Parámetros:
|
|
28
|
+
- dataframe1: Primer DataFrame de Pandas.
|
|
29
|
+
- dataframe2: Segundo DataFrame de Pandas.
|
|
30
|
+
- lefton: Lista de columnas clave en dataframe1.
|
|
31
|
+
- righton: Lista de columnas clave en dataframe2.
|
|
32
|
+
|
|
33
|
+
Retorna:
|
|
34
|
+
- dfmerge: DataFrame con la unión.
|
|
35
|
+
- df_no_match_left: Registros en dataframe1 que no encontraron match en dataframe2.
|
|
36
|
+
- df_no_match_right: Registros en dataframe2 que no encontraron match en dataframe1.
|
|
37
|
+
- alerta_duplicados: Mensaje de alerta si hay duplicados en la clave.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Realizar el merge con how='left'
|
|
41
|
+
if lefton == righton:
|
|
42
|
+
dfmerge = pd.merge(dataframe1, dataframe2, on=lefton, how='left',suffixes=('','_right'))
|
|
43
|
+
else:
|
|
44
|
+
dfmerge = pd.merge(dataframe1, dataframe2, left_on=lefton, right_on=righton, how='left', suffixes=('','_right'))
|
|
45
|
+
|
|
46
|
+
# Eliminar columnas duplicadas generadas por el merge
|
|
47
|
+
if isinstance(righton, list): # Si righton es una lista de columnas
|
|
48
|
+
for col in righton:
|
|
49
|
+
columna_duplicada = f"{col}_right"
|
|
50
|
+
if columna_duplicada in dfmerge.columns:
|
|
51
|
+
dfmerge = dfmerge.drop(columns=[columna_duplicada])
|
|
52
|
+
else: # Si righton es una sola columna
|
|
53
|
+
columna_duplicada = f"{righton}_right"
|
|
54
|
+
if columna_duplicada in dfmerge.columns:
|
|
55
|
+
dfmerge = dfmerge.drop(columns=[columna_duplicada])
|
|
56
|
+
|
|
57
|
+
# Detectar registros de df1 que no tienen coincidencias en df2 (valores NaN en columnas de df2)
|
|
58
|
+
columnas_df2 = [col for col in dataframe2.columns if col not in righton]
|
|
59
|
+
df_no_match_left = dfmerge[dfmerge[columnas_df2].isna().all(axis=1)]
|
|
60
|
+
|
|
61
|
+
# Detectar registros de df2 que no tienen coincidencias en df1
|
|
62
|
+
claves_merge = set(dfmerge[lefton].drop_duplicates().itertuples(index=False, name=None))
|
|
63
|
+
df_no_match_right = dataframe2[~dataframe2[righton].apply(lambda x: tuple(x) in claves_merge, axis=1)]
|
|
64
|
+
|
|
65
|
+
# Detectar duplicados en las claves del merge
|
|
66
|
+
duplicados = dfmerge.groupby(lefton).size()
|
|
67
|
+
duplicados = duplicados[duplicados > 1] # Filtrar claves duplicadas
|
|
68
|
+
|
|
69
|
+
alerta_duplicados = "⚠️ Hay duplicados en la clave del cruce" if len(duplicados) > 0 else "✅ No hay duplicados en la clave"
|
|
70
|
+
|
|
71
|
+
return dfmerge, df_no_match_left, df_no_match_right, alerta_duplicados
|
|
72
|
+
|
|
73
|
+
################################################################################################################################
|
|
74
|
+
################################################################################################################################
|
|
75
|
+
################################### Quita las tildes del contenido de un DataFrame ############################################
|
|
76
|
+
################################################################################################################################
|
|
77
|
+
################################################################################################################################
|
|
78
|
+
def dfSinTildes(dataframe):
|
|
79
|
+
for columna in dataframe.columns:
|
|
80
|
+
if dataframe[columna].dtype == 'object':
|
|
81
|
+
dataframe[columna] = dataframe[columna].apply(unidecode)
|
|
82
|
+
return dataframe
|
|
83
|
+
|
|
84
|
+
################################################################################################################################
|
|
85
|
+
################################################################################################################################
|
|
86
|
+
################################### Quita las tildes de los encabezados de un DataFrame ######################################
|
|
87
|
+
################################################################################################################################
|
|
88
|
+
################################################################################################################################
|
|
89
|
+
def dfEncabezadosSinTildes(dataframe):
|
|
90
|
+
nuevos_nombres_columnas = [unidecode(columna)
|
|
91
|
+
if dataframe[columna].dtype == 'object' else columna for columna in dataframe.columns]
|
|
92
|
+
dataframe.columns = nuevos_nombres_columnas
|
|
93
|
+
return dataframe
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
################################################################################################################################
|
|
97
|
+
################################################################################################################################
|
|
98
|
+
############################### Guarda un df como un excel en caso de existir reemplaza ######################################
|
|
99
|
+
################################################################################################################################
|
|
100
|
+
################################################################################################################################
|
|
101
|
+
def GuardarRemplazarExcel(dataframe,rutaParaGuardado,nombreHoja, ejecutar=True):
|
|
102
|
+
if ejecutar:
|
|
103
|
+
if os.path.exists(rutaParaGuardado):
|
|
104
|
+
os.remove(rutaParaGuardado)
|
|
105
|
+
dataframe.to_excel(rutaParaGuardado,sheet_name=nombreHoja, index=False)
|
|
106
|
+
|
|
107
|
+
################################################################################################################################
|
|
108
|
+
################################################################################################################################
|
|
109
|
+
############################### Guarda un df como un CSV en caso de existir reemplaza ########################################
|
|
110
|
+
################################################################################################################################
|
|
111
|
+
################################################################################################################################
|
|
112
|
+
def GuardarRemplazarCsv(dataframe,rutaParaGuardado, ejecutar=True):
|
|
113
|
+
if ejecutar:
|
|
114
|
+
if os.path.exists(rutaParaGuardado):
|
|
115
|
+
os.remove(rutaParaGuardado)
|
|
116
|
+
dataframe.to_csv(rutaParaGuardado, index=False)
|
|
117
|
+
|
|
118
|
+
################################################################################################################################
|
|
119
|
+
################################################################################################################################
|
|
120
|
+
############################### Guarda un df como un parquet en caso de existir reemplaza ####################################
|
|
121
|
+
################################################################################################################################
|
|
122
|
+
################################################################################################################################
|
|
123
|
+
def GuardarRemplazarParquet(dataframe,rutaParaGuardado, ejecutar=True, engine="fastparquet"):
|
|
124
|
+
if ejecutar:
|
|
125
|
+
if os.path.exists(rutaParaGuardado):
|
|
126
|
+
os.remove(rutaParaGuardado)
|
|
127
|
+
if engine=="":
|
|
128
|
+
dataframe.to_parquet(rutaParaGuardado, index=False)
|
|
129
|
+
else:
|
|
130
|
+
dataframe.to_parquet(rutaParaGuardado, index=False, engine=engine)
|
|
131
|
+
|
|
132
|
+
################################################################################################################################
|
|
133
|
+
################################################################################################################################
|
|
134
|
+
######################################################### Convierte columnas en tipo bool ####################################
|
|
135
|
+
################################################################################################################################
|
|
136
|
+
################################################################################################################################
|
|
137
|
+
def EstandarizarColumnasConDatosBool(dataframe):
|
|
138
|
+
for col in dataframe.columns:
|
|
139
|
+
# Si en la columna hay al menos un valor booleano
|
|
140
|
+
if dataframe[col].apply(lambda x: isinstance(x, bool)).any():
|
|
141
|
+
# Reemplazar NaN con False y convertir todo a tipo bool
|
|
142
|
+
dataframe[col] = dataframe[col].astype(object).fillna(False).astype(bool)
|
|
143
|
+
return dataframe
|
|
144
|
+
|
|
145
|
+
################################################################################################################################
|
|
146
|
+
################################################################################################################################
|
|
147
|
+
######################################################### Convierte columnas en tipo bool ####################################
|
|
148
|
+
################################################################################################################################
|
|
149
|
+
################################################################################################################################
|
|
150
|
+
def extraerMesInicioDelNombre(nombreArchivo,Separador):
|
|
151
|
+
""" Extrae el número y nombre del mes desde el nombre del archivo. """
|
|
152
|
+
nombreArchivo = nombreArchivo.replace(" ", "") # Elimina espacios extra
|
|
153
|
+
partes = nombreArchivo.split(Separador)
|
|
154
|
+
if len(partes) >= 1:
|
|
155
|
+
numero_mes = partes[0].strip() # "01"
|
|
156
|
+
return numero_mes
|
|
157
|
+
|
|
158
|
+
return None, None # Retornar valores nulos si el formato no es el esperado
|
|
159
|
+
|
|
160
|
+
################################################################################################################################
|
|
161
|
+
################################################################################################################################
|
|
162
|
+
################ Funcion que convierte una cadena a (UpperCamelCase), eliminando espacios y guiones bajos. ###################
|
|
163
|
+
################################################################################################################################
|
|
164
|
+
################################################################################################################################
|
|
165
|
+
def pasarACamelCase(texto,Separador):
|
|
166
|
+
# Reemplaza guiones bajos por espacios, divide en palabras, y las capitaliza
|
|
167
|
+
partes = texto.replace(Separador, " ").split()
|
|
168
|
+
return ''.join(p.capitalize() for p in partes)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
################################################################################################################################
|
|
172
|
+
################################################################################################################################
|
|
173
|
+
################ Renombrar columnas de un df con poniendo en CamelCase lo que esta a la izquierda del separador ###############
|
|
174
|
+
################################################################################################################################
|
|
175
|
+
################################################################################################################################
|
|
176
|
+
def renombraColumnasTablas(dataframe,Separador):
|
|
177
|
+
nuevas_columnas = {}
|
|
178
|
+
for col in dataframe.columns:
|
|
179
|
+
if "_" in col:
|
|
180
|
+
izquierda, derecha = col.split("_", 1) # Solo divide en el primer guion bajo
|
|
181
|
+
nuevo_nombre = f"{pasarACamelCase(izquierda,Separador)}_{derecha}"
|
|
182
|
+
else:
|
|
183
|
+
nuevo_nombre = pasarACamelCase(col,Separador)
|
|
184
|
+
nuevas_columnas[col] = nuevo_nombre
|
|
185
|
+
return dataframe.rename(columns=nuevas_columnas)
|
motulco/doc_utils.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from unidecode import unidecode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
################################################################################################################################
|
|
9
|
+
################################################################################################################################
|
|
10
|
+
####################################### Borrar los archivos si existen en la carpeta ###########################################
|
|
11
|
+
################################################################################################################################
|
|
12
|
+
################################################################################################################################
|
|
13
|
+
def BorrrarArchivo(rutaArchivo):
|
|
14
|
+
if os.path.exists(rutaArchivo):# Verificar si el archivo ya existe
|
|
15
|
+
os.remove(rutaArchivo)
|
|
16
|
+
|
|
17
|
+
################################################################################################################################
|
|
18
|
+
################################################################################################################################
|
|
19
|
+
################################# Mueve los archivos descargados a la carpeta que se requiere ##################################
|
|
20
|
+
################################################################################################################################
|
|
21
|
+
################################################################################################################################
|
|
22
|
+
def moverArchivo(RutaOrigen, RutaDestino):
|
|
23
|
+
try:
|
|
24
|
+
shutil.move(RutaOrigen, RutaDestino)
|
|
25
|
+
print(f"El archivo '{RutaOrigen}' se ha movido correctamente a '{RutaDestino}'.")
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"No se pudo mover el archivo '{RutaOrigen}' a '{RutaDestino}': {e}")
|
|
28
|
+
|
|
29
|
+
################################################################################################################################
|
|
30
|
+
################################################################################################################################
|
|
31
|
+
############################################### Copia un archivo de un destino a otro ##########################################
|
|
32
|
+
################################################################################################################################
|
|
33
|
+
################################################################################################################################
|
|
34
|
+
def copiarArchivo(RutaOrigen, RutaDestino):
|
|
35
|
+
try:
|
|
36
|
+
shutil.copy2(RutaOrigen, RutaDestino) # `copy2` copia metadatos (marca de tiempo) además del archivo
|
|
37
|
+
print(f"El archivo '{RutaOrigen}' se ha copiado correctamente a '{RutaDestino}'.")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"No se pudo copiar el archivo '{RutaOrigen}' a '{RutaDestino}': {e}")
|
|
40
|
+
|
|
41
|
+
################################################################################################################################
|
|
42
|
+
################################################################################################################################
|
|
43
|
+
###################################### Obtiene el archivo más reciente de una carpeta ##########################################
|
|
44
|
+
################################################################################################################################
|
|
45
|
+
################################################################################################################################
|
|
46
|
+
def obtenerArchivoMasReciente(rutaCarpeta, formato=".txt"):
|
|
47
|
+
# Obtener la lista de archivos en la carpeta
|
|
48
|
+
archivos_en_carpeta = [archivo for archivo in os.listdir(rutaCarpeta) if archivo.endswith(formato)]
|
|
49
|
+
# Verificar si hay archivos en la carpeta
|
|
50
|
+
if archivos_en_carpeta:
|
|
51
|
+
# Obtener el archivo más reciente basado en la última modificación
|
|
52
|
+
archivo_mas_reciente = max(archivos_en_carpeta,key=lambda x: os.path.getmtime(os.path.join(rutaCarpeta, x)))
|
|
53
|
+
# Obtener la fecha de modificación del archivo más reciente
|
|
54
|
+
fecha_modificacion = os.path.getmtime(os.path.join(rutaCarpeta, archivo_mas_reciente))
|
|
55
|
+
# Retornar nombre y fecha
|
|
56
|
+
return archivo_mas_reciente, fecha_modificacion
|
|
57
|
+
else:
|
|
58
|
+
return None, None
|
|
59
|
+
|
|
60
|
+
################################################################################################################################
|
|
61
|
+
################################################################################################################################
|
|
62
|
+
###################################### Retorna la ruta que exista del arreglo de rutas #########################################
|
|
63
|
+
################################################################################################################################
|
|
64
|
+
################################################################################################################################
|
|
65
|
+
def ExisteRuta(arregloDeRutas):
|
|
66
|
+
carpeta_correcta = None
|
|
67
|
+
for carpeta in arregloDeRutas:
|
|
68
|
+
try:
|
|
69
|
+
# Verifica si la ruta existe y es una carpeta
|
|
70
|
+
if os.path.isdir(carpeta):
|
|
71
|
+
print(f"La carpeta existe en: {carpeta}")
|
|
72
|
+
carpeta_correcta = carpeta
|
|
73
|
+
break # Sale del ciclo si la carpeta existe
|
|
74
|
+
except Exception as e:
|
|
75
|
+
# Maneja otras posibles excepciones (e.g., problemas de permisos)
|
|
76
|
+
print(f"Error al verificar la carpeta {carpeta}: {e}")
|
|
77
|
+
continue
|
|
78
|
+
if carpeta_correcta is None:
|
|
79
|
+
print("La carpeta no se encontró en ninguna de las rutas proporcionadas.")
|
|
80
|
+
else:
|
|
81
|
+
# Aquí puedes continuar con el procesamiento usando la carpeta_correcta
|
|
82
|
+
pass
|
|
83
|
+
return carpeta_correcta
|
|
84
|
+
|
motulco/excel_utils.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import openpyxl
|
|
4
|
+
import time
|
|
5
|
+
import pythoncom
|
|
6
|
+
import win32com.client
|
|
7
|
+
from openpyxl import load_workbook
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
################################################################################################################################
|
|
11
|
+
################################################################################################################################
|
|
12
|
+
############################################### Carga un archivo de excel como un df ###########################################
|
|
13
|
+
################################################################################################################################
|
|
14
|
+
################################################################################################################################
|
|
15
|
+
def cargaExcel(rutaArchivo,sheetName, AsString=False,engine='openpyxl'):
|
|
16
|
+
df = pd.read_excel(rutaArchivo, sheet_name=sheetName, engine=engine)
|
|
17
|
+
if AsString:
|
|
18
|
+
df=df.astype(str)
|
|
19
|
+
return df
|
|
20
|
+
|
|
21
|
+
################################################################################################################################
|
|
22
|
+
################################################################################################################################
|
|
23
|
+
############################################# Ejecuta la funcion Actualizar todo de excel ######################################
|
|
24
|
+
################################################################################################################################
|
|
25
|
+
################################################################################################################################
|
|
26
|
+
|
|
27
|
+
def ActualizarTodoExcel(rutaArchivo):
|
|
28
|
+
pythoncom.CoInitialize() # Inicializa COM correctamente
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
rutaArchivoAbsoluta = os.path.abspath(rutaArchivo)
|
|
32
|
+
print(f"Intentando abrir el archivo: {rutaArchivoAbsoluta}")
|
|
33
|
+
|
|
34
|
+
if not os.path.exists(rutaArchivoAbsoluta):
|
|
35
|
+
print(f"⚠️ El archivo no existe en la ruta: {rutaArchivoAbsoluta}")
|
|
36
|
+
return
|
|
37
|
+
# Iniciar Excel y hacerlo visible
|
|
38
|
+
excel_app = win32com.client.DispatchEx("Excel.Application")
|
|
39
|
+
excel_app.Visible = True
|
|
40
|
+
excel_app.DisplayAlerts = False
|
|
41
|
+
# Abrir el archivo de Excel
|
|
42
|
+
workbook = excel_app.Workbooks.Open(rutaArchivoAbsoluta)
|
|
43
|
+
print("📂 Archivo abierto correctamente, iniciando actualización...")
|
|
44
|
+
|
|
45
|
+
# Activar la primera hoja (por si acaso)
|
|
46
|
+
workbook.Sheets(1).Activate()
|
|
47
|
+
# Iniciar actualización
|
|
48
|
+
workbook.RefreshAll()
|
|
49
|
+
print("🔄 Actualizando conexiones y tablas de Power Query...")
|
|
50
|
+
# Esperar que Excel termine la actualización y cálculos
|
|
51
|
+
while excel_app.CalculationState != 0: # 0 significa "listo"
|
|
52
|
+
print("⌛ Esperando que Excel termine los cálculos...")
|
|
53
|
+
time.sleep(2)
|
|
54
|
+
print("✅ Actualización completada.")
|
|
55
|
+
# Guardar y cerrar Excel
|
|
56
|
+
print(f'📁 Archivo actualizado y cerrado: {rutaArchivoAbsoluta}')
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"❌ Error al actualizar el archivo {rutaArchivoAbsoluta}: {e}")
|
|
59
|
+
finally:
|
|
60
|
+
# Asegurar que Excel se cierra correctamente
|
|
61
|
+
if 'excel_app' in locals():
|
|
62
|
+
a=10
|
|
63
|
+
pythoncom.CoUninitialize()
|
|
64
|
+
|
|
65
|
+
################################################################################################################################
|
|
66
|
+
################################################################################################################################
|
|
67
|
+
#################################### Consolida Archivos de excel cuyo nombre es semejante ######################################
|
|
68
|
+
################################################################################################################################
|
|
69
|
+
################################################################################################################################
|
|
70
|
+
def consolidarArchivosExcelPrefijo(NombreConElQueIniciaLosArchivos,RutaALaCarpeta,TipoArchivos='.xlsx'):
|
|
71
|
+
dataframes = []
|
|
72
|
+
for archivo in os.listdir(RutaALaCarpeta):
|
|
73
|
+
if archivo.endswith(TipoArchivos) and archivo.startswith(NombreConElQueIniciaLosArchivos):
|
|
74
|
+
ruta_archivo = os.path.join(RutaALaCarpeta, archivo)
|
|
75
|
+
df = pd.read_excel(ruta_archivo)
|
|
76
|
+
dataframes.append(df)
|
|
77
|
+
df_consolidado = pd.concat(dataframes, ignore_index=True)
|
|
78
|
+
return df_consolidado
|
|
79
|
+
|
|
80
|
+
################################################################################################################################
|
|
81
|
+
################################################################################################################################
|
|
82
|
+
############################################## Consolida Archivos de excel en una carpeta ######################################
|
|
83
|
+
################################################################################################################################
|
|
84
|
+
################################################################################################################################
|
|
85
|
+
def consolidarArchivosExcelGlobal(NombreConElQueIniciaLosArchivos,RutaALaCarpeta,TipoArchivos='.xlsx'):
|
|
86
|
+
dataframes = []
|
|
87
|
+
for archivo in os.listdir(RutaALaCarpeta):
|
|
88
|
+
if archivo.endswith(TipoArchivos):
|
|
89
|
+
ruta_archivo = os.path.join(RutaALaCarpeta, archivo)
|
|
90
|
+
df = pd.read_excel(ruta_archivo)
|
|
91
|
+
dataframes.append(df)
|
|
92
|
+
df_consolidado = pd.concat(dataframes, ignore_index=True)
|
|
93
|
+
return df_consolidado
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
################################################################################################################################
|
|
97
|
+
################################################################################################################################
|
|
98
|
+
################################## Funcion que retornas el arreglo de hojas visibles (no oculta) en excel ###################
|
|
99
|
+
################################################################################################################################
|
|
100
|
+
################################################################################################################################
|
|
101
|
+
def obtenerHojasVisiblesExcel(RutaArchivo):
|
|
102
|
+
"""Obtiene solo las hojas visibles de un archivo Excel (.xlsx)."""
|
|
103
|
+
try:
|
|
104
|
+
wb = load_workbook(RutaArchivo, read_only=True)
|
|
105
|
+
hojas_visibles = [sheet.title for sheet in wb.worksheets if sheet.sheet_state == "visible"]
|
|
106
|
+
wb.close()
|
|
107
|
+
return hojas_visibles
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"Error al leer hojas de {RutaArchivo}: {e}")
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
################################################################################################################################
|
|
113
|
+
################################################################################################################################
|
|
114
|
+
################################## Funcion que retornas el arreglo de todas las hojas en excel ##############################
|
|
115
|
+
################################################################################################################################
|
|
116
|
+
################################################################################################################################
|
|
117
|
+
def obtenerTodasLasHojasExcel(RutaArchivo):
|
|
118
|
+
"""Obtiene todas las hojas de un archivo Excel (.xlsx), sin importar si están visibles u ocultas."""
|
|
119
|
+
try:
|
|
120
|
+
wb = load_workbook(RutaArchivo, read_only=True)
|
|
121
|
+
hojas = [sheet.title for sheet in wb.worksheets] # Todas las hojas
|
|
122
|
+
wb.close()
|
|
123
|
+
return hojas
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Error al leer hojas de {RutaArchivo}: {e}")
|
|
126
|
+
return []
|
motulco/pdf_utils.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pypdf import PdfReader, PdfWriter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
################################################################################################################################
|
|
6
|
+
################################################################################################################################
|
|
7
|
+
####################################### Rota los grados y paginas deseadas de un pdf ###########################################
|
|
8
|
+
################################################################################################################################
|
|
9
|
+
################################################################################################################################
|
|
10
|
+
def rotarPaginasPdf(pdf_entrada, pdf_salida, paginas_a_rotar, grados):
|
|
11
|
+
"""
|
|
12
|
+
Rota páginas específicas de un PDF.
|
|
13
|
+
|
|
14
|
+
pdf_entrada: str -> ruta del PDF original
|
|
15
|
+
pdf_salida: str -> ruta del PDF modificado
|
|
16
|
+
paginas_a_rotar: list[int] -> índices de páginas a rotar (0 = primera)
|
|
17
|
+
grados: int -> grados de rotación (90, 180, 270)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
reader = PdfReader(pdf_entrada)
|
|
21
|
+
writer = PdfWriter()
|
|
22
|
+
|
|
23
|
+
for i, page in enumerate(reader.pages):
|
|
24
|
+
if i in paginas_a_rotar:
|
|
25
|
+
page.rotate(grados)
|
|
26
|
+
writer.add_page(page)
|
|
27
|
+
|
|
28
|
+
with open(pdf_salida, "wb") as f:
|
|
29
|
+
writer.write(f)
|
motulco/ppt_utils.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
from PIL import Image
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
################################################################################################################################
|
|
9
|
+
################################################################################################################################
|
|
10
|
+
################################################# Abre una diapositiva existente ###############################################
|
|
11
|
+
################################################################################################################################
|
|
12
|
+
################################################################################################################################
|
|
13
|
+
def abrirPresentacion(RutaPpt):
|
|
14
|
+
# --- 1️⃣ Limpiar la caché COM dañada ---
|
|
15
|
+
gen_py_path = os.path.join(os.environ["LOCALAPPDATA"], "Temp", "gen_py")
|
|
16
|
+
if os.path.exists(gen_py_path):
|
|
17
|
+
try:
|
|
18
|
+
shutil.rmtree(gen_py_path, ignore_errors=True)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
print(f"No se pudo eliminar la caché gen_py: {e}")
|
|
21
|
+
|
|
22
|
+
# --- 2️⃣ Regenerar caché de COM ---
|
|
23
|
+
import win32com.client.gencache
|
|
24
|
+
win32com.client.gencache.is_readonly = False
|
|
25
|
+
try:
|
|
26
|
+
win32com.client.gencache.Rebuild()
|
|
27
|
+
except Exception:
|
|
28
|
+
pass # A veces da advertencias inofensivas
|
|
29
|
+
|
|
30
|
+
# --- 3️⃣ Crear o usar instancia de PowerPoint ---
|
|
31
|
+
try:
|
|
32
|
+
ppt_app = win32com.client.GetActiveObject("PowerPoint.Application")
|
|
33
|
+
except Exception:
|
|
34
|
+
ppt_app = win32com.client.Dispatch("PowerPoint.Application")
|
|
35
|
+
|
|
36
|
+
ppt_app.Visible = True
|
|
37
|
+
|
|
38
|
+
# --- 4️⃣ Abrir la presentación plantilla ---
|
|
39
|
+
pres_src = ppt_app.Presentations.Open(RutaPpt)
|
|
40
|
+
|
|
41
|
+
return ppt_app, pres_src
|
|
42
|
+
|
|
43
|
+
################################################################################################################################
|
|
44
|
+
################################################################################################################################
|
|
45
|
+
################################################# Crea un libro de ppt en blanco ###############################################
|
|
46
|
+
################################################################################################################################
|
|
47
|
+
################################################################################################################################
|
|
48
|
+
def CrearPresentacion(pptDestino):
|
|
49
|
+
import time
|
|
50
|
+
import win32com.client as win32
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
ppt_app = win32.gencache.EnsureDispatch("PowerPoint.Application")
|
|
54
|
+
except Exception:
|
|
55
|
+
# Si falla, limpia la caché y usa Dispatch
|
|
56
|
+
import os, shutil
|
|
57
|
+
gen_py_path = os.path.join(os.environ["LOCALAPPDATA"], "Temp", "gen_py")
|
|
58
|
+
shutil.rmtree(gen_py_path, ignore_errors=True)
|
|
59
|
+
ppt_app = win32.Dispatch("PowerPoint.Application")
|
|
60
|
+
|
|
61
|
+
ppt_app.Visible = True
|
|
62
|
+
pres_dst = ppt_app.Presentations.Add()
|
|
63
|
+
pres_dst.SaveAs(pptDestino)
|
|
64
|
+
time.sleep(1)
|
|
65
|
+
return ppt_app, pres_dst
|
|
66
|
+
|
|
67
|
+
################################################################################################################################
|
|
68
|
+
################################################################################################################################
|
|
69
|
+
################################################# Copia diapositivas entre ppt's ###############################################
|
|
70
|
+
################################################################################################################################
|
|
71
|
+
################################################################################################################################
|
|
72
|
+
def copiarDiaposPosicion(pres_src, pres_dst, posicionesCopiar):
|
|
73
|
+
# ✅ Ajustar tamaño de página igual al origen
|
|
74
|
+
pres_dst.PageSetup.SlideWidth = pres_src.PageSetup.SlideWidth
|
|
75
|
+
pres_dst.PageSetup.SlideHeight = pres_src.PageSetup.SlideHeight
|
|
76
|
+
|
|
77
|
+
for pos in posicionesCopiar:
|
|
78
|
+
pres_src.Slides(pos + 1).Copy() # Copiar (recordar que es base 1)
|
|
79
|
+
pres_dst.Slides.Paste() # Pegar manteniendo formato
|
|
80
|
+
|
|
81
|
+
#pres_dst.Save() # ✅ Guardar cambios
|
|
82
|
+
|
|
83
|
+
################################################################################################################################
|
|
84
|
+
################################################################################################################################
|
|
85
|
+
######################################################## Guarda una presentacion ###############################################
|
|
86
|
+
################################################################################################################################
|
|
87
|
+
################################################################################################################################
|
|
88
|
+
def guardarPpt(pres_dst):
|
|
89
|
+
pres_dst.Save()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
################################################################################################################################
|
|
93
|
+
################################################################################################################################
|
|
94
|
+
#################################### Obtienene la ultima posicion Vertical de la ppt ###########################################
|
|
95
|
+
################################################################################################################################
|
|
96
|
+
################################################################################################################################
|
|
97
|
+
def getNextY(slide, padding=5):
|
|
98
|
+
""" Calcula la siguiente posición disponible en Y dentro de una diapositiva """
|
|
99
|
+
max_y = 0
|
|
100
|
+
for shape in slide.Shapes:
|
|
101
|
+
bottom = shape.Top + shape.Height
|
|
102
|
+
if bottom > max_y:
|
|
103
|
+
max_y = bottom
|
|
104
|
+
return max_y + padding
|
|
105
|
+
|
|
106
|
+
################################################################################################################################
|
|
107
|
+
################################################################################################################################
|
|
108
|
+
########################################## Obtienene la cantidad de slides en la ppt ###########################################
|
|
109
|
+
################################################################################################################################
|
|
110
|
+
################################################################################################################################
|
|
111
|
+
def obtenerDiapositivaMaxima(presentacion):
|
|
112
|
+
try:
|
|
113
|
+
return presentacion.Slides.Count
|
|
114
|
+
except:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
################################################################################################################################
|
|
120
|
+
################################################################################################################################
|
|
121
|
+
################################################# Agregar Elemento (shape) a una ppt ###########################################
|
|
122
|
+
################################################################################################################################
|
|
123
|
+
################################################################################################################################
|
|
124
|
+
def agregarElemento(pres_dst, pres_src, num_slide, tipo, contenido, x=50, y="auto",
|
|
125
|
+
ancho=300, alto=100, font_size=20,
|
|
126
|
+
font_name="Arial", bold=False, italic=False,
|
|
127
|
+
color=(0, 0, 0), align="left",
|
|
128
|
+
crop_top=0, crop_bottom=0, crop_left=0, crop_right=0,
|
|
129
|
+
auto_crop=False, mantener_relacion=True, mantener_relacion_base="ancho",
|
|
130
|
+
paddingSet=5, rotacion=0):
|
|
131
|
+
"""
|
|
132
|
+
Inserta texto o imagen en una diapositiva de PowerPoint.
|
|
133
|
+
Ahora incluye rotación para imágenes vía rotacion=grados.
|
|
134
|
+
"""
|
|
135
|
+
puntosACentimetros=28.3465
|
|
136
|
+
slide = pres_dst.Slides(num_slide)
|
|
137
|
+
slide_width = pres_dst.PageSetup.SlideWidth
|
|
138
|
+
slide_height = pres_dst.PageSetup.SlideHeight
|
|
139
|
+
|
|
140
|
+
# ======== Calcular Y inicial ========
|
|
141
|
+
if y == "auto":
|
|
142
|
+
y = getNextY(slide, paddingSet) - crop_top * puntosACentimetros * 6
|
|
143
|
+
|
|
144
|
+
# ======== Auto crop ========
|
|
145
|
+
if auto_crop:
|
|
146
|
+
crop_top = 0.05
|
|
147
|
+
crop_bottom = 0.05
|
|
148
|
+
crop_left = 0.03
|
|
149
|
+
crop_right = 0.03
|
|
150
|
+
|
|
151
|
+
# ======== Mantener relación si es imagen ========
|
|
152
|
+
if tipo == "imagen" and mantener_relacion:
|
|
153
|
+
try:
|
|
154
|
+
with Image.open(contenido) as img:
|
|
155
|
+
width_orig, height_orig = img.size
|
|
156
|
+
aspect_ratio = width_orig / height_orig
|
|
157
|
+
|
|
158
|
+
if mantener_relacion_base == "ancho":
|
|
159
|
+
# Ajustar alto usando el ancho como referencia
|
|
160
|
+
alto = ancho / aspect_ratio
|
|
161
|
+
|
|
162
|
+
elif mantener_relacion_base == "alto":
|
|
163
|
+
# Ajustar ancho usando el alto como referencia
|
|
164
|
+
ancho = alto * aspect_ratio
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
print(f"⚠️ Opción inválida en mantener_relacion_base: {mantener_relacion_base}")
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"⚠️ No se pudo calcular proporción de imagen: {e}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ======== Calcular alto visible ========
|
|
174
|
+
alto_visible = alto * (1 - crop_top - crop_bottom)
|
|
175
|
+
|
|
176
|
+
# ======== Verificar si cabe ========
|
|
177
|
+
if y + alto_visible > slide_height:
|
|
178
|
+
copiarDiaposPosicion(pres_src, pres_dst, [0])
|
|
179
|
+
num_slide += 1
|
|
180
|
+
slide = pres_dst.Slides(num_slide)
|
|
181
|
+
y = getNextY(slide, paddingSet) - crop_top * puntosACentimetros * 6
|
|
182
|
+
|
|
183
|
+
# ======== Convertir color HEX a RGB ========
|
|
184
|
+
if isinstance(color, str):
|
|
185
|
+
color = hex_to_rgb(color)
|
|
186
|
+
|
|
187
|
+
# ======== TEXTO / TÍTULO ========
|
|
188
|
+
if tipo in ["texto", "titulo"]:
|
|
189
|
+
textbox = slide.Shapes.AddTextbox(1, x if x != "center" else 50, y, ancho, alto)
|
|
190
|
+
text_range = textbox.TextFrame.TextRange
|
|
191
|
+
text_range.Text = contenido
|
|
192
|
+
text_range.Font.Name = font_name
|
|
193
|
+
text_range.Font.Size = font_size
|
|
194
|
+
text_range.Font.Bold = -1 if bold else 0
|
|
195
|
+
text_range.Font.Italic = -1 if italic else 0
|
|
196
|
+
text_range.Font.Color.RGB = color[0] + (color[1] << 8) + (color[2] << 16)
|
|
197
|
+
|
|
198
|
+
if align == "center":
|
|
199
|
+
textbox.TextFrame.TextRange.ParagraphFormat.Alignment = 2
|
|
200
|
+
elif align == "right":
|
|
201
|
+
textbox.TextFrame.TextRange.ParagraphFormat.Alignment = 3
|
|
202
|
+
elif align == "justify":
|
|
203
|
+
textbox.TextFrame.TextRange.ParagraphFormat.Alignment = 4
|
|
204
|
+
else:
|
|
205
|
+
textbox.TextFrame.TextRange.ParagraphFormat.Alignment = 1
|
|
206
|
+
|
|
207
|
+
if tipo == "titulo":
|
|
208
|
+
text_range.Font.Bold = -1
|
|
209
|
+
|
|
210
|
+
# ⭐ Aplicar rotación a texto
|
|
211
|
+
try:
|
|
212
|
+
textbox.Rotation = float(rotacion)
|
|
213
|
+
except:
|
|
214
|
+
print(f"⚠️ No se pudo aplicar rotación a texto: {rotacion}")
|
|
215
|
+
|
|
216
|
+
return textbox
|
|
217
|
+
|
|
218
|
+
# ======== IMAGEN ========
|
|
219
|
+
elif tipo == "imagen":
|
|
220
|
+
shape = slide.Shapes.AddPicture(
|
|
221
|
+
contenido, LinkToFile=False, SaveWithDocument=True,
|
|
222
|
+
Left=0, Top=y, Width=ancho, Height=alto
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
shape.LockAspectRatio = mantener_relacion
|
|
226
|
+
|
|
227
|
+
# --- Aplicar recortes ---
|
|
228
|
+
shape.PictureFormat.CropTop = crop_top * shape.Height
|
|
229
|
+
shape.PictureFormat.CropBottom = crop_bottom * shape.Height
|
|
230
|
+
shape.PictureFormat.CropLeft = crop_left * shape.Width
|
|
231
|
+
shape.PictureFormat.CropRight = crop_right * shape.Width
|
|
232
|
+
|
|
233
|
+
# --- Centrar horizontalmente ---
|
|
234
|
+
if isinstance(x, str) and x.lower() in ["center", "x_center"]:
|
|
235
|
+
ancho_real = shape.Width
|
|
236
|
+
shape.Left = (slide_width - ancho_real) / 2
|
|
237
|
+
else:
|
|
238
|
+
shape.Left = x
|
|
239
|
+
|
|
240
|
+
# --- ⭐ APLICAR ROTACIÓN ⭐ ---
|
|
241
|
+
try:
|
|
242
|
+
shape.Rotation = float(rotacion)
|
|
243
|
+
except:
|
|
244
|
+
print(f"⚠️ No se pudo aplicar rotación: {rotacion}")
|
|
245
|
+
|
|
246
|
+
return shape
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
################################################################################################################################
|
|
250
|
+
################################################################################################################################
|
|
251
|
+
################################################# Agregar Elemento (shape) a una ppt ###########################################
|
|
252
|
+
################################################################################################################################
|
|
253
|
+
################################################################################################################################
|
|
254
|
+
def AgregarElementoConindicador(Indicador, tipo,df_Control,PresDest,PresSrc,contenidoAdicional):
|
|
255
|
+
puntosACentimetros=28.3465
|
|
256
|
+
if df_Control.at[Indicador, 'INSERTAR SLIDE']:
|
|
257
|
+
copiarDiaposPosicion(PresSrc, PresDest, [0])
|
|
258
|
+
|
|
259
|
+
if contenidoAdicional =="" and tipo != "imagen":
|
|
260
|
+
contenidoFinal=df_Control.at[Indicador, 'CONTENIDO']
|
|
261
|
+
elif contenidoAdicional !="" and tipo != "imagen":
|
|
262
|
+
if pd.isna(df_Control.at[Indicador, 'CONTENIDO']) or str(df_Control.at[Indicador, 'CONTENIDO']).strip().lower() in ["nan", "none", ""]:
|
|
263
|
+
contenidoFinal=f"{contenidoAdicional}"
|
|
264
|
+
else:
|
|
265
|
+
contenidoFinal=df_Control.at[Indicador, 'CONTENIDO'] + f" {contenidoAdicional}"
|
|
266
|
+
|
|
267
|
+
if tipo == "imagen":
|
|
268
|
+
agregarElemento(pres_dst=PresDest,pres_src=PresSrc,num_slide = PresDest.Slides.Count ,tipo= df_Control.at[Indicador, 'TIPO'],
|
|
269
|
+
contenido=os.path.abspath(df_Control.at[Indicador, 'CONTENIDO']) ,x=convertirPuntosACentimetros(df_Control.at[Indicador, 'POSICION X']),
|
|
270
|
+
y=convertirPuntosACentimetros(df_Control.at[Indicador, 'POSICION Y']) ,ancho=df_Control.at[Indicador, 'ANCHO'] * puntosACentimetros,
|
|
271
|
+
alto=df_Control.at[Indicador, 'ALTO'] * puntosACentimetros ,font_size=df_Control.at[Indicador, 'TAMANIO FUENTE'],
|
|
272
|
+
font_name=df_Control.at[Indicador, 'TIPO FUENTE'] ,bold=df_Control.at[Indicador, 'NEGRITA'], italic=df_Control.at[Indicador, 'CURSIVA']
|
|
273
|
+
,color=df_Control.at[Indicador, 'COLOR'], align=df_Control.at[Indicador, 'ALINEACION'] ,crop_top=df_Control.at[Indicador, 'CORTE SUPERIOR']
|
|
274
|
+
,crop_bottom=df_Control.at[Indicador, 'CORTE INFERIOR'] ,crop_left=df_Control.at[Indicador, 'CORTE IZQUIERDA'],
|
|
275
|
+
crop_right=df_Control.at[Indicador, 'CORTE DERECHA'] ,auto_crop=df_Control.at[Indicador, 'AUTO CORTE']
|
|
276
|
+
,mantener_relacion=df_Control.at[Indicador, 'RELACION ASPECTO'],paddingSet=df_Control.at[Indicador, 'PADDING'],
|
|
277
|
+
rotacion=df_Control.at[Indicador, 'ROTACION'],mantener_relacion_base=df_Control.at[Indicador, 'BASE RELACION ASPECTO'])
|
|
278
|
+
else:
|
|
279
|
+
agregarElemento(pres_dst=PresDest,pres_src=PresSrc,num_slide = PresDest.Slides.Count ,tipo= df_Control.at[Indicador, 'TIPO'],
|
|
280
|
+
contenido=contenidoFinal ,x=convertirPuntosACentimetros(df_Control.at[Indicador, 'POSICION X']), y=convertirPuntosACentimetros(df_Control.at[Indicador, 'POSICION Y']),
|
|
281
|
+
ancho=df_Control.at[Indicador, 'ANCHO'] * puntosACentimetros, alto=df_Control.at[Indicador, 'ALTO'] * puntosACentimetros,
|
|
282
|
+
font_size=df_Control.at[Indicador, 'TAMANIO FUENTE'], font_name=df_Control.at[Indicador, 'TIPO FUENTE'] ,bold=df_Control.at[Indicador, 'NEGRITA'],
|
|
283
|
+
italic=df_Control.at[Indicador, 'CURSIVA'] ,color=df_Control.at[Indicador, 'COLOR'], align=df_Control.at[Indicador, 'ALINEACION'],
|
|
284
|
+
crop_top=df_Control.at[Indicador, 'CORTE SUPERIOR'], crop_bottom=df_Control.at[Indicador, 'CORTE INFERIOR'],
|
|
285
|
+
crop_left=df_Control.at[Indicador, 'CORTE IZQUIERDA'], crop_right=df_Control.at[Indicador, 'CORTE DERECHA'],
|
|
286
|
+
auto_crop=df_Control.at[Indicador, 'AUTO CORTE'], mantener_relacion=df_Control.at[Indicador, 'RELACION ASPECTO'],
|
|
287
|
+
paddingSet=df_Control.at[Indicador, 'PADDING'],rotacion=df_Control.at[Indicador, 'ROTACION'])
|
|
288
|
+
|
|
289
|
+
################################################################################################################################
|
|
290
|
+
################################################################################################################################
|
|
291
|
+
################################################# Convierte de puntos a centimetros ############################################
|
|
292
|
+
################################################################################################################################
|
|
293
|
+
################################################################################################################################
|
|
294
|
+
def convertirPuntosACentimetros(value):
|
|
295
|
+
puntosACentimetros=28.3465
|
|
296
|
+
try:
|
|
297
|
+
return (float(value) * puntosACentimetros)
|
|
298
|
+
except (ValueError, TypeError):
|
|
299
|
+
return str(value)
|
|
300
|
+
|
|
301
|
+
################################################################################################################################
|
|
302
|
+
################################################################################################################################
|
|
303
|
+
####################################################### Convierte de HEX a RGB #################################################
|
|
304
|
+
################################################################################################################################
|
|
305
|
+
################################################################################################################################
|
|
306
|
+
def hex_to_rgb(hex_color):
|
|
307
|
+
"""Convierte color hex (#RRGGBB) a tupla RGB (R,G,B)"""
|
|
308
|
+
hex_color = hex_color.lstrip("#")
|
|
309
|
+
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
motulco/py_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os, shutil, win32com.client, importlib
|
|
2
|
+
import nbformat
|
|
3
|
+
from nbconvert.preprocessors import ExecutePreprocessor
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
################################################################################################################################
|
|
7
|
+
################################################################################################################################
|
|
8
|
+
################################################# Limpia el Caché ##############################################################
|
|
9
|
+
################################################################################################################################
|
|
10
|
+
################################################################################################################################
|
|
11
|
+
def limpiarCache():
|
|
12
|
+
# Rutas posibles de caché
|
|
13
|
+
paths = [
|
|
14
|
+
os.path.join(os.environ["LOCALAPPDATA"], "Temp", "gen_py"),
|
|
15
|
+
os.path.join(os.path.dirname(win32com.__file__), "gen_py"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
for path in paths:
|
|
19
|
+
if os.path.exists(path):
|
|
20
|
+
print(f"Eliminando caché: {path}")
|
|
21
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
22
|
+
else:
|
|
23
|
+
print(f"No existe: {path}")
|
|
24
|
+
|
|
25
|
+
# Forzar limpieza interna del gencache
|
|
26
|
+
try:
|
|
27
|
+
win32com.client.gencache.is_readonly = False
|
|
28
|
+
win32com.client.gencache.Rebuild()
|
|
29
|
+
print("🔄 Caché reconstruida.")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print("⚠️ No se pudo reconstruir automáticamente:", e)
|
|
32
|
+
|
|
33
|
+
################################################################################################################################
|
|
34
|
+
################################################################################################################################
|
|
35
|
+
################################################# Ejecuta un ipynb desde otro codigo ###########################################
|
|
36
|
+
################################################################################################################################
|
|
37
|
+
################################################################################################################################
|
|
38
|
+
def ejecutar_notebook(ruta_notebook):
|
|
39
|
+
"""
|
|
40
|
+
Ejecuta un notebook Jupyter (.ipynb) en la ruta proporcionada.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
# Cargar el notebook
|
|
44
|
+
with open(ruta_notebook, 'r', encoding='utf-8') as f:
|
|
45
|
+
notebook = nbformat.read(f, as_version=4)
|
|
46
|
+
|
|
47
|
+
# Preprocesador para ejecutar el notebook
|
|
48
|
+
ep = ExecutePreprocessor(timeout=600, kernel_name='python3')
|
|
49
|
+
|
|
50
|
+
# Ejecutar el notebook
|
|
51
|
+
ep.preprocess(notebook, {'metadata': {'path': Path(ruta_notebook).parent}})
|
|
52
|
+
print(f"Notebook ejecutado correctamente: {ruta_notebook}")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"Error al ejecutar {ruta_notebook}: {e}")
|
motulco/shapes_utils.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import os
|
|
5
|
+
from html2image import Html2Image
|
|
6
|
+
from PIL import Image
|
|
7
|
+
from PIL import ImageChops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
################################################################################################################################
|
|
11
|
+
################################################################################################################################
|
|
12
|
+
########################################## Generacion de graficos de Barras y Lineas ###########################################
|
|
13
|
+
################################################################################################################################
|
|
14
|
+
################################################################################################################################
|
|
15
|
+
|
|
16
|
+
def graficoBarrasYLineas(meses, InfoBarras=None, InfoLineas=None,
|
|
17
|
+
coloresBarras=["#19BDE6", "#152C47", "#BACF00"],
|
|
18
|
+
titulo="", ruta_salida="grafico.png", mostrar_tabla=True,
|
|
19
|
+
ancho_figura=14, alto_figura=6, labelLineas="Resultado Real",
|
|
20
|
+
colorLinea="#A461AB", AnchoGrafica=0.8, desplazamientoLeyenda=-0.25,
|
|
21
|
+
BarrasPorcentaje=False, LineasPorcentaje=False,
|
|
22
|
+
cantidadDecimBarras="0", cantidadDecimLineas="0"):
|
|
23
|
+
|
|
24
|
+
x = np.arange(len(meses))
|
|
25
|
+
fig, ax1 = plt.subplots(figsize=(ancho_figura, alto_figura))
|
|
26
|
+
|
|
27
|
+
# Decidir si deben compartir eje (cuando ambos tienen mismo tipo: ambos % o ambos absolutos)
|
|
28
|
+
share_axis = (BarrasPorcentaje == LineasPorcentaje)
|
|
29
|
+
|
|
30
|
+
# --- METAS (barras) ---
|
|
31
|
+
handles1, labels1 = [], []
|
|
32
|
+
if InfoBarras is not None and len(InfoBarras) > 0:
|
|
33
|
+
width = AnchoGrafica / len(InfoBarras) if len(InfoBarras) > 0 else AnchoGrafica
|
|
34
|
+
for i, (meta_name, valores_raw) in enumerate(InfoBarras.items()):
|
|
35
|
+
# Asegurarse que sean floats
|
|
36
|
+
valores = [float(v) for v in valores_raw]
|
|
37
|
+
|
|
38
|
+
# Multiplicar por 100 para graficar si es porcentaje
|
|
39
|
+
plot_vals = [v * 100 for v in valores] if BarrasPorcentaje else valores
|
|
40
|
+
|
|
41
|
+
ax1.bar(
|
|
42
|
+
x + (i - len(InfoBarras) / 2) * width + width / 2,
|
|
43
|
+
plot_vals,
|
|
44
|
+
width,
|
|
45
|
+
label=meta_name,
|
|
46
|
+
color=coloresBarras[i % len(coloresBarras)]
|
|
47
|
+
)
|
|
48
|
+
handles1, labels1 = ax1.get_legend_handles_labels()
|
|
49
|
+
|
|
50
|
+
# --- RESULTADOS REALES (líneas) ---
|
|
51
|
+
handles2, labels2 = [], []
|
|
52
|
+
eje_leyenda = ax1 # por defecto
|
|
53
|
+
|
|
54
|
+
if InfoLineas is not None and len(InfoLineas) > 0:
|
|
55
|
+
# decidir ax2: si comparten eje -> ax2 = ax1; si no -> crear twin sólo si hace falta
|
|
56
|
+
if share_axis:
|
|
57
|
+
ax2 = ax1
|
|
58
|
+
else:
|
|
59
|
+
# si las lineas están en porcentaje pero las barras no, o viceversa, usar twin
|
|
60
|
+
ax2 = ax1.twinx() if LineasPorcentaje else ax1
|
|
61
|
+
|
|
62
|
+
if isinstance(InfoLineas, pd.DataFrame):
|
|
63
|
+
num_lineas = InfoLineas.shape[1]
|
|
64
|
+
labels = labelLineas if isinstance(labelLineas, list) else InfoLineas.columns.tolist()
|
|
65
|
+
colores = colorLinea if isinstance(colorLinea, list) else [colorLinea] * num_lineas
|
|
66
|
+
|
|
67
|
+
for i, col in enumerate(InfoLineas.columns):
|
|
68
|
+
vals_raw = InfoLineas[col].astype(float).values
|
|
69
|
+
vals = [float(v) for v in vals_raw]
|
|
70
|
+
|
|
71
|
+
# Multiplicar por 100 si es porcentaje
|
|
72
|
+
if LineasPorcentaje:
|
|
73
|
+
vals = [v * 100 for v in vals]
|
|
74
|
+
|
|
75
|
+
ax2.plot(x, vals, marker="o", linewidth=2, color=colores[i % len(colores)], label=labels[i])
|
|
76
|
+
|
|
77
|
+
# Etiquetas de los puntos
|
|
78
|
+
max_val = max(vals) if len(vals) > 0 else 0.0
|
|
79
|
+
for j, val in enumerate(vals):
|
|
80
|
+
ax2.text(
|
|
81
|
+
x[j], val + max_val * 0.03,
|
|
82
|
+
f"{val:.{cantidadDecimLineas}f}%" if LineasPorcentaje else f"{val:.{cantidadDecimLineas}f}",
|
|
83
|
+
ha="center", fontsize=10, color="white",
|
|
84
|
+
bbox=dict(facecolor=colores[i % len(colores)], edgecolor="none", boxstyle="round,pad=0.25")
|
|
85
|
+
)
|
|
86
|
+
eje_leyenda = ax2
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
# Caso 1 sola lista
|
|
90
|
+
vals = [float(v) for v in InfoLineas]
|
|
91
|
+
|
|
92
|
+
if LineasPorcentaje:
|
|
93
|
+
vals = [v * 100 for v in vals]
|
|
94
|
+
ax2.plot(x, vals, marker="o", linewidth=2, color=colorLinea,
|
|
95
|
+
label=labelLineas if isinstance(labelLineas, str) else labelLineas[0])
|
|
96
|
+
|
|
97
|
+
max_val = max(vals) if len(vals) > 0 else 0.0
|
|
98
|
+
for i, val in enumerate(vals):
|
|
99
|
+
ax2.text(
|
|
100
|
+
x[i], val + max_val * 0.03,
|
|
101
|
+
f"{val:.{cantidadDecimLineas}f}%" if LineasPorcentaje else f"{val:.{cantidadDecimLineas}f}",
|
|
102
|
+
ha="center", fontsize=8, color="white",
|
|
103
|
+
bbox=dict(facecolor=colorLinea if isinstance(colorLinea, str) else colorLinea[0],
|
|
104
|
+
edgecolor="none", boxstyle="round,pad=0.25")
|
|
105
|
+
)
|
|
106
|
+
eje_leyenda = ax2
|
|
107
|
+
|
|
108
|
+
# Combinar leyendas
|
|
109
|
+
handles2, labels2 = eje_leyenda.get_legend_handles_labels()
|
|
110
|
+
labels_final, handles_final = [], []
|
|
111
|
+
for h, l in zip(handles1 + handles2, labels1 + labels2):
|
|
112
|
+
if l not in labels_final:
|
|
113
|
+
labels_final.append(l)
|
|
114
|
+
handles_final.append(h)
|
|
115
|
+
|
|
116
|
+
# --- TÍTULO ---
|
|
117
|
+
plt.title(titulo, fontsize=13, pad=25)
|
|
118
|
+
|
|
119
|
+
# --- LEYENDA ---
|
|
120
|
+
handles_final, labels_final = [], []
|
|
121
|
+
|
|
122
|
+
if 'handles1' in locals():
|
|
123
|
+
handles_final += handles1
|
|
124
|
+
labels_final += labels1
|
|
125
|
+
if 'handles2' in locals():
|
|
126
|
+
handles_final += handles2
|
|
127
|
+
labels_final += labels2
|
|
128
|
+
|
|
129
|
+
if handles_final:
|
|
130
|
+
plt.legend(handles_final, labels_final,
|
|
131
|
+
loc="upper center", bbox_to_anchor=(0.5, desplazamientoLeyenda),
|
|
132
|
+
ncol=4, fontsize=8, frameon=False)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --- AJUSTAR ESCALAS ---
|
|
136
|
+
# Si comparten eje (ambos % o ambos absolutos) forzar mismos límites
|
|
137
|
+
if InfoLineas is not None:
|
|
138
|
+
if share_axis:
|
|
139
|
+
# obtener límites actuales (si ax2 es el mismo que ax1 esto es redundante pero seguro)
|
|
140
|
+
try:
|
|
141
|
+
# si existe ax2 y es distinto a ax1 (por seguridad), considerar ambos
|
|
142
|
+
if 'ax2' in locals() and ax2 is not ax1:
|
|
143
|
+
min_y = min(ax1.get_ylim()[0], ax2.get_ylim()[0])
|
|
144
|
+
max_y = max(ax1.get_ylim()[1], ax2.get_ylim()[1])
|
|
145
|
+
else:
|
|
146
|
+
min_y, max_y = ax1.get_ylim()
|
|
147
|
+
ax1.set_ylim(min_y, max_y)
|
|
148
|
+
if 'ax2' in locals() and ax2 is not ax1:
|
|
149
|
+
ax2.set_ylim(min_y, max_y)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass # si falla por algún motivo, no detener el plotting
|
|
152
|
+
|
|
153
|
+
# --- QUITAR EJES ---
|
|
154
|
+
ax1.axis("off")
|
|
155
|
+
if 'ax2' in locals() and (ax2 is not ax1) and InfoLineas is not None and LineasPorcentaje:
|
|
156
|
+
ax2.axis("off")
|
|
157
|
+
|
|
158
|
+
# --- TABLA ---
|
|
159
|
+
if mostrar_tabla and (InfoBarras is not None or InfoLineas is not None):
|
|
160
|
+
tabla_data = []
|
|
161
|
+
row_labels = []
|
|
162
|
+
|
|
163
|
+
# Barras
|
|
164
|
+
if InfoBarras is not None:
|
|
165
|
+
for fila_raw in InfoBarras.values():
|
|
166
|
+
fila = [float(v) for v in fila_raw]
|
|
167
|
+
fila_formateada = [
|
|
168
|
+
f"{val * 100:,.{cantidadDecimBarras}f}%" if BarrasPorcentaje else f"{val:,.{cantidadDecimBarras}f}"
|
|
169
|
+
for val in fila
|
|
170
|
+
]
|
|
171
|
+
tabla_data.append(fila_formateada)
|
|
172
|
+
row_labels += list(InfoBarras.keys())
|
|
173
|
+
|
|
174
|
+
# Líneas
|
|
175
|
+
if InfoLineas is not None:
|
|
176
|
+
if isinstance(InfoLineas, pd.DataFrame):
|
|
177
|
+
for i, col in enumerate(InfoLineas.columns):
|
|
178
|
+
vals = InfoLineas[col].astype(float).values
|
|
179
|
+
if LineasPorcentaje:
|
|
180
|
+
vals = [v * 100 for v in vals]
|
|
181
|
+
fila_resultado = [f"{v:,.{cantidadDecimLineas}f}%" for v in vals]
|
|
182
|
+
else:
|
|
183
|
+
fila_resultado = [f"{v:,.{cantidadDecimLineas}f}" for v in vals]
|
|
184
|
+
tabla_data.append(fila_resultado)
|
|
185
|
+
row_labels += (labelLineas if isinstance(labelLineas, list) else InfoLineas.columns.tolist())
|
|
186
|
+
else:
|
|
187
|
+
vals = [float(v) for v in InfoLineas]
|
|
188
|
+
if LineasPorcentaje:
|
|
189
|
+
vals = [v * 100 for v in vals]
|
|
190
|
+
fila_resultado = [f"{v:,.{cantidadDecimLineas}f}%" for v in vals]
|
|
191
|
+
else:
|
|
192
|
+
fila_resultado = [f"{v:,.{cantidadDecimLineas}f}" for v in vals]
|
|
193
|
+
tabla_data.append(fila_resultado)
|
|
194
|
+
row_labels.append(labelLineas if isinstance(labelLineas, str) else labelLineas[0])
|
|
195
|
+
|
|
196
|
+
# Crear tabla
|
|
197
|
+
tabla = plt.table(cellText=tabla_data, rowLabels=row_labels, colLabels=meses,
|
|
198
|
+
cellLoc="center", loc="bottom")
|
|
199
|
+
tabla.auto_set_font_size(False)
|
|
200
|
+
tabla.set_fontsize(10) # Mas tamaño
|
|
201
|
+
tabla.scale(1, 1.5) #Ancho y alto
|
|
202
|
+
plt.subplots_adjust(left=0.05, bottom=0.30, top=0.80)
|
|
203
|
+
|
|
204
|
+
# --- GUARDAR ---
|
|
205
|
+
plt.tight_layout()
|
|
206
|
+
plt.savefig(ruta_salida.replace(".png", ".svg"), format="svg", bbox_inches="tight")
|
|
207
|
+
plt.close()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
################################################################################################################################
|
|
212
|
+
################################################################################################################################
|
|
213
|
+
######################################################## Generacion de tablas #################################################
|
|
214
|
+
################################################################################################################################
|
|
215
|
+
################################################################################################################################
|
|
216
|
+
|
|
217
|
+
def generar_tabla(
|
|
218
|
+
df,tendenciasPorColumna=None,mostrarFlechas=True,separador_miles=False,color_encabezado="#0a4d8c",color_texto_encabezado="#ffffff",
|
|
219
|
+
color_fondo="#ffffff",color_borde="#cccccc",ruta_imagen=None,ajusteColumnas=0,ajusteTexto=0,anchoPrimeraColumna=160,
|
|
220
|
+
anchoOtrasColumnas=90,resaltarUltimaFila=True,negritaUltimaFila=True,colores_celdas=None # 🆕 NUEVO
|
|
221
|
+
):
|
|
222
|
+
df = df.copy()
|
|
223
|
+
|
|
224
|
+
# 1) Tendencias
|
|
225
|
+
tendencias_calculadas = {}
|
|
226
|
+
if tendenciasPorColumna is not None:
|
|
227
|
+
for col, modo in tendenciasPorColumna.items():
|
|
228
|
+
if modo == "auto":
|
|
229
|
+
tendencias_calculadas[col] = df[col].apply(
|
|
230
|
+
lambda x: "subió" if x > 0 else ("bajó" if x < 0 else "mantuvo")
|
|
231
|
+
)
|
|
232
|
+
elif isinstance(modo, list):
|
|
233
|
+
tendencias_calculadas[col] = modo
|
|
234
|
+
|
|
235
|
+
# 2) Formato de valores
|
|
236
|
+
def aplicar_formato(valor, col):
|
|
237
|
+
if pd.isna(valor):
|
|
238
|
+
return ""
|
|
239
|
+
if isinstance(valor, bool):
|
|
240
|
+
return "<span style='color:green; font-weight:bold; font-size:18px;'>✅</span>" if valor else "<span style='color:red; font-weight:bold; font-size:18px;'>❌</span>"
|
|
241
|
+
elif mostrarFlechas and col in tendencias_calculadas:
|
|
242
|
+
idx = df.index[df[col] == valor][0]
|
|
243
|
+
tendencia = tendencias_calculadas[col][idx]
|
|
244
|
+
if tendencia == "subió":
|
|
245
|
+
return f"<span style='color:green;'>▲ {valor:.1f}%</span>"
|
|
246
|
+
elif tendencia == "bajó":
|
|
247
|
+
return f"<span style='color:red;'>▼ {valor:.1f}%</span>"
|
|
248
|
+
else:
|
|
249
|
+
return f"<span style='color:gray;'>• {valor:.1f}%</span>"
|
|
250
|
+
elif isinstance(valor, (int, float)) and "%" in col:
|
|
251
|
+
return f"{valor:.1f}%"
|
|
252
|
+
elif separador_miles and isinstance(valor, (int, float)):
|
|
253
|
+
return (
|
|
254
|
+
f"{valor:,.0f}".replace(",", ".")
|
|
255
|
+
if abs(valor) >= 1000
|
|
256
|
+
else (f"{valor:.1f}" if valor != int(valor) else f"{int(valor)}")
|
|
257
|
+
)
|
|
258
|
+
elif isinstance(valor, (int, float)):
|
|
259
|
+
return f"{valor:.1f}"
|
|
260
|
+
else:
|
|
261
|
+
return valor
|
|
262
|
+
|
|
263
|
+
for col in df.columns:
|
|
264
|
+
df[col] = df[col].apply(lambda x: aplicar_formato(x, col))
|
|
265
|
+
|
|
266
|
+
# 3) Estilos base
|
|
267
|
+
font_size_base = 14 + ajusteTexto
|
|
268
|
+
ultima_fila_bg = "#f2f2f2" if resaltarUltimaFila else color_fondo
|
|
269
|
+
ultima_fila_fw = "bold" if negritaUltimaFila else "normal"
|
|
270
|
+
|
|
271
|
+
estilos = f"""
|
|
272
|
+
<style>
|
|
273
|
+
html, body {{
|
|
274
|
+
margin: 0;
|
|
275
|
+
padding: 0;
|
|
276
|
+
background: transparent;
|
|
277
|
+
}}
|
|
278
|
+
.tabla-tiv {{
|
|
279
|
+
border-collapse: collapse;
|
|
280
|
+
font-family: Arial, sans-serif;
|
|
281
|
+
font-size: {font_size_base}px;
|
|
282
|
+
color: #000000;
|
|
283
|
+
background-color: {color_fondo};
|
|
284
|
+
table-layout: fixed;
|
|
285
|
+
margin: 0;
|
|
286
|
+
}}
|
|
287
|
+
.tabla-tiv th, .tabla-tiv td {{
|
|
288
|
+
border: 1px solid {color_borde};
|
|
289
|
+
padding: 6px;
|
|
290
|
+
text-align: center;
|
|
291
|
+
vertical-align: middle;
|
|
292
|
+
word-wrap: break-word;
|
|
293
|
+
box-sizing: border-box;
|
|
294
|
+
}}
|
|
295
|
+
.tabla-tiv th {{
|
|
296
|
+
background-color: {color_encabezado};
|
|
297
|
+
color: {color_texto_encabezado};
|
|
298
|
+
font-weight: bold;
|
|
299
|
+
}}
|
|
300
|
+
.tabla-tiv tr:last-child td {{
|
|
301
|
+
background-color: {ultima_fila_bg};
|
|
302
|
+
font-weight: {ultima_fila_fw};
|
|
303
|
+
}}
|
|
304
|
+
.tabla-tiv th:first-child, .tabla-tiv td:first-child {{
|
|
305
|
+
width: {anchoPrimeraColumna + ajusteColumnas}px;
|
|
306
|
+
text-align: left;
|
|
307
|
+
padding-left: 10px;
|
|
308
|
+
}}
|
|
309
|
+
.tabla-tiv th:not(:first-child), .tabla-tiv td:not(:first-child) {{
|
|
310
|
+
width: {anchoOtrasColumnas + ajusteColumnas}px;
|
|
311
|
+
}}
|
|
312
|
+
</style>
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
html_tabla = estilos + df.to_html(index=False, escape=False, classes="tabla-tiv")
|
|
316
|
+
|
|
317
|
+
# 🎨 NUEVO: Colores por celda (acepta lista o DataFrame)
|
|
318
|
+
if colores_celdas is not None:
|
|
319
|
+
|
|
320
|
+
if isinstance(colores_celdas, pd.DataFrame):
|
|
321
|
+
matriz = colores_celdas.values.tolist()
|
|
322
|
+
else:
|
|
323
|
+
matriz = colores_celdas
|
|
324
|
+
|
|
325
|
+
filas, columnas = df.shape
|
|
326
|
+
|
|
327
|
+
if len(matriz) != filas or any(len(fila) != columnas for fila in matriz):
|
|
328
|
+
raise ValueError("colores_celdas debe tener exactamente las mismas dimensiones que df")
|
|
329
|
+
|
|
330
|
+
estilos_celdas = "<style>\n"
|
|
331
|
+
for i in range(filas):
|
|
332
|
+
for j in range(columnas):
|
|
333
|
+
color = matriz[i][j]
|
|
334
|
+
if color:
|
|
335
|
+
estilos_celdas += f"""
|
|
336
|
+
.tabla-tiv tbody tr:nth-child({i+1}) td:nth-child({j+1}) {{
|
|
337
|
+
background-color: {color} !important;
|
|
338
|
+
}}
|
|
339
|
+
"""
|
|
340
|
+
estilos_celdas += "</style>\n"
|
|
341
|
+
|
|
342
|
+
html_tabla = estilos_celdas + html_tabla
|
|
343
|
+
|
|
344
|
+
# 4) Captura imagen
|
|
345
|
+
if ruta_imagen:
|
|
346
|
+
os.makedirs(os.path.dirname(ruta_imagen), exist_ok=True)
|
|
347
|
+
hti = Html2Image(output_path=os.path.dirname(ruta_imagen))
|
|
348
|
+
hti.browser.flags = [
|
|
349
|
+
"--hide-scrollbars",
|
|
350
|
+
"--no-sandbox",
|
|
351
|
+
"--disable-gpu",
|
|
352
|
+
"--force-device-scale-factor=1"
|
|
353
|
+
]
|
|
354
|
+
hti.screenshot(
|
|
355
|
+
html_str=html_tabla,
|
|
356
|
+
save_as=os.path.basename(ruta_imagen)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return html_tabla
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
################################################################################################################################
|
|
363
|
+
################################################################################################################################
|
|
364
|
+
################################################# Recortar los bordes blancos #################################################
|
|
365
|
+
################################################################################################################################
|
|
366
|
+
################################################################################################################################
|
|
367
|
+
|
|
368
|
+
def recortar_espacios_blancos(ruta_entrada, ruta_salida=None):
|
|
369
|
+
img = Image.open(ruta_entrada)
|
|
370
|
+
# Convertir la imagen a modo que permita detectar el fondo
|
|
371
|
+
img_sin_alpha = img.convert("RGB")
|
|
372
|
+
# Crear una imagen en blanco del mismo tamaño
|
|
373
|
+
fondo = Image.new("RGB", img_sin_alpha.size, (255, 255, 255))
|
|
374
|
+
# Calcular diferencia
|
|
375
|
+
diff = Image.eval(ImageChops.difference(img_sin_alpha, fondo), lambda x: 255 if x else 0)
|
|
376
|
+
bbox = diff.getbbox() # Encuentra el área sin blanco
|
|
377
|
+
if bbox:
|
|
378
|
+
img_recortada = img.crop(bbox)
|
|
379
|
+
if ruta_salida:
|
|
380
|
+
img_recortada.save(ruta_salida)
|
|
381
|
+
return img_recortada
|
|
382
|
+
return img
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
################################################################################################################################
|
|
386
|
+
################################################################################################################################
|
|
387
|
+
######################################################### Girar una imagen ####################################################
|
|
388
|
+
################################################################################################################################
|
|
389
|
+
################################################################################################################################
|
|
390
|
+
|
|
391
|
+
def rotar_imagen_python(ruta_imagen, grados, ruta_salida=None):
|
|
392
|
+
"""
|
|
393
|
+
Rota una imagen directamente en Python usando PIL.
|
|
394
|
+
Retorna la ruta de la imagen rotada.
|
|
395
|
+
"""
|
|
396
|
+
try:
|
|
397
|
+
img = Image.open(ruta_imagen)
|
|
398
|
+
img_rotada = img.rotate(grados, expand=True) # expand evita recortes
|
|
399
|
+
|
|
400
|
+
if ruta_salida is None:
|
|
401
|
+
# genera nombre temporal
|
|
402
|
+
ruta_salida = ruta_imagen.replace(".", f"_rotada_{grados}.")
|
|
403
|
+
|
|
404
|
+
img_rotada.save(ruta_salida)
|
|
405
|
+
return ruta_salida
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print(f"Error rotando la imagen: {e}")
|
|
409
|
+
return ruta_imagen
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: motulco
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Librería de Python para manejo de archivos y DataFrames de Daniel Ochoa
|
|
5
|
+
Author-email: DanielOchoa <df.ochoa202@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pandas>=2.0.0
|
|
10
|
+
Requires-Dist: numpy>=1.26.0
|
|
11
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
12
|
+
Requires-Dist: unidecode>=1.3.6
|
|
13
|
+
Requires-Dist: pywin32>=306
|
|
14
|
+
Requires-Dist: pypdf>=4.0.0
|
|
15
|
+
Requires-Dist: Pillow>=10.0.0
|
|
16
|
+
Requires-Dist: nbformat>=5.9.0
|
|
17
|
+
Requires-Dist: nbconvert>=7.9.0
|
|
18
|
+
Requires-Dist: matplotlib>=3.8.0
|
|
19
|
+
Requires-Dist: html2image>=2.0.4
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# motulco
|
|
23
|
+
Librería de Python para manejo de archivos y procesamiento de DataFrames.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
motulco/__init__.py,sha256=zRd3Kfp3CIJxaGOA9RKaiLbbWCS5OyK8QkUip-AZMZM,205
|
|
2
|
+
motulco/colors_utils.py,sha256=jqGLyzBD2rDfSadbqiHxr0Q-fkjiqckmoBRJuvf0gu4,846
|
|
3
|
+
motulco/df_utils.py,sha256=5WE0y3h-6GmQZi4W1DaZkYzD0xySFqVSVA9S-OKG_bY,12844
|
|
4
|
+
motulco/doc_utils.py,sha256=ieT0tnhO1U6bY9ZjIeQBwf09iMRZWerbZ6nKLaqcx9Q,5717
|
|
5
|
+
motulco/excel_utils.py,sha256=7yzohXl1debV-uxKRc0HTSU33wNYL3f0yIrhMmOBmUg,7801
|
|
6
|
+
motulco/pdf_utils.py,sha256=21BUKGZfg23Upb94Hsn1FDehuMpX_xZ2m9060iHxxWQ,1335
|
|
7
|
+
motulco/ppt_utils.py,sha256=U-alkXanoxm-0sKggKhxDUv4--HbQ6SsTNyGbpL-AuQ,17986
|
|
8
|
+
motulco/py_utils.py,sha256=5LidHC8A0o95HSziVLqDSPme5UO3TuoiPQhy_KLEXpA,2874
|
|
9
|
+
motulco/shapes_utils.py,sha256=GIRUXaw6emSGD8kcmRICUBN7ZqMbiV6rQp3-cnPLzdE,18366
|
|
10
|
+
motulco-0.2.0.dist-info/licenses/LICENSE,sha256=Jn96Lhnfqd-Zr3dFIJhaDlIZJSk-pbfnZ6sGlp0Gv5E,12
|
|
11
|
+
motulco-0.2.0.dist-info/METADATA,sha256=I7CE_v-OPojUrG8vZGHKmppRezIFmNERLKjDLFEE7l8,735
|
|
12
|
+
motulco-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
motulco-0.2.0.dist-info/top_level.txt,sha256=vmdmHWbWDEBabUTgsBx7i5muUhJECnn7y8JfCMki6Gg,8
|
|
14
|
+
motulco-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT License
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
motulco
|