motulco 0.2.2__tar.gz

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-0.2.2/LICENSE ADDED
@@ -0,0 +1 @@
1
+ MIT License
motulco-0.2.2/PKG-INFO ADDED
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: motulco
3
+ Version: 0.2.2
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,2 @@
1
+ # motulco
2
+ Librería de Python para manejo de archivos y procesamiento de DataFrames.
@@ -0,0 +1,8 @@
1
+ from .colors_utils import *
2
+ from .df_utils import *
3
+ from .doc_utils import *
4
+ from .excel_utils import *
5
+ from .pdf_utils import *
6
+ from .ppt_utils import *
7
+ from .py_utils import *
8
+ from .shapes_utils import *
@@ -0,0 +1,12 @@
1
+
2
+
3
+
4
+ ################################################################################################################################
5
+ ################################################################################################################################
6
+ ####################################################### Convierte de HEX a RGB #################################################
7
+ ################################################################################################################################
8
+ ################################################################################################################################
9
+ def hexToRgb(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))
@@ -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)
@@ -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
+
@@ -0,0 +1,155 @@
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
+ ############################################### Carga un archivo de excel como un df ###########################################
12
+ ################################################################################################################################
13
+ ################################################################################################################################
14
+ def cargaExcel(rutaArchivo,sheetName, AsString=False,engine='openpyxl'):
15
+ df = pd.read_excel(rutaArchivo, sheet_name=sheetName, engine=engine)
16
+ if AsString:
17
+ df=df.astype(str)
18
+ return df
19
+
20
+ ################################################################################################################################
21
+ ################################################################################################################################
22
+ ############################################# Ejecuta la funcion Actualizar todo de excel ######################################
23
+ ################################################################################################################################
24
+ ################################################################################################################################
25
+
26
+ def ActualizarTodoExcel(rutaArchivo):
27
+ pythoncom.CoInitialize() # Inicializa COM correctamente
28
+
29
+ try:
30
+ rutaArchivoAbsoluta = os.path.abspath(rutaArchivo)
31
+ print(f"Intentando abrir el archivo: {rutaArchivoAbsoluta}")
32
+
33
+ if not os.path.exists(rutaArchivoAbsoluta):
34
+ print(f"⚠️ El archivo no existe en la ruta: {rutaArchivoAbsoluta}")
35
+ return
36
+ # Iniciar Excel y hacerlo visible
37
+ excel_app = win32com.client.DispatchEx("Excel.Application")
38
+ excel_app.Visible = True
39
+ excel_app.DisplayAlerts = False
40
+ # Abrir el archivo de Excel
41
+ workbook = excel_app.Workbooks.Open(rutaArchivoAbsoluta)
42
+ print("📂 Archivo abierto correctamente, iniciando actualización...")
43
+
44
+ # Activar la primera hoja (por si acaso)
45
+ workbook.Sheets(1).Activate()
46
+ # Iniciar actualización
47
+ workbook.RefreshAll()
48
+ print("🔄 Actualizando conexiones y tablas de Power Query...")
49
+ # Esperar que Excel termine la actualización y cálculos
50
+ while excel_app.CalculationState != 0: # 0 significa "listo"
51
+ print("⌛ Esperando que Excel termine los cálculos...")
52
+ time.sleep(2)
53
+ print("✅ Actualización completada.")
54
+ # Guardar y cerrar Excel
55
+ print(f'📁 Archivo actualizado y cerrado: {rutaArchivoAbsoluta}')
56
+ except Exception as e:
57
+ print(f"❌ Error al actualizar el archivo {rutaArchivoAbsoluta}: {e}")
58
+ finally:
59
+ # Asegurar que Excel se cierra correctamente
60
+ if 'excel_app' in locals():
61
+ a=10
62
+ pythoncom.CoUninitialize()
63
+
64
+ ################################################################################################################################
65
+ ################################################################################################################################
66
+ #################################### Consolida Archivos de excel cuyo nombre es semejante ######################################
67
+ ################################################################################################################################
68
+ ################################################################################################################################
69
+ def consolidarArchivosExcelPrefijo(NombreConElQueIniciaLosArchivos,RutaALaCarpeta,TipoArchivos='.xlsx'):
70
+ dataframes = []
71
+ for archivo in os.listdir(RutaALaCarpeta):
72
+ if archivo.endswith(TipoArchivos) and archivo.startswith(NombreConElQueIniciaLosArchivos):
73
+ ruta_archivo = os.path.join(RutaALaCarpeta, archivo)
74
+ df = pd.read_excel(ruta_archivo)
75
+ dataframes.append(df)
76
+ df_consolidado = pd.concat(dataframes, ignore_index=True)
77
+ return df_consolidado
78
+
79
+ ################################################################################################################################
80
+ ################################################################################################################################
81
+ ############################################## Consolida Archivos de excel en una carpeta ######################################
82
+ ################################################################################################################################
83
+ ################################################################################################################################
84
+ def consolidarArchivosExcelGlobal(NombreConElQueIniciaLosArchivos,RutaALaCarpeta,TipoArchivos='.xlsx'):
85
+ dataframes = []
86
+ for archivo in os.listdir(RutaALaCarpeta):
87
+ if archivo.endswith(TipoArchivos):
88
+ ruta_archivo = os.path.join(RutaALaCarpeta, archivo)
89
+ df = pd.read_excel(ruta_archivo)
90
+ dataframes.append(df)
91
+ df_consolidado = pd.concat(dataframes, ignore_index=True)
92
+ return df_consolidado
93
+
94
+
95
+ ################################################################################################################################
96
+ ################################################################################################################################
97
+ ################################## Funcion que retornas el arreglo de hojas visibles (no oculta) en excel ###################
98
+ ################################################################################################################################
99
+ ################################################################################################################################
100
+ def obtenerHojasVisiblesExcel(RutaArchivo):
101
+ """Obtiene solo las hojas visibles de un archivo Excel (.xlsx)."""
102
+ try:
103
+ wb = load_workbook(RutaArchivo, read_only=True)
104
+ hojas_visibles = [sheet.title for sheet in wb.worksheets if sheet.sheet_state == "visible"]
105
+ wb.close()
106
+ return hojas_visibles
107
+ except Exception as e:
108
+ print(f"Error al leer hojas de {RutaArchivo}: {e}")
109
+ return []
110
+
111
+ ################################################################################################################################
112
+ ################################################################################################################################
113
+ ################################## Funcion que retornas el arreglo de todas las hojas en excel ##############################
114
+ ################################################################################################################################
115
+ ################################################################################################################################
116
+ def obtenerTodasLasHojasExcel(RutaArchivo):
117
+ """Obtiene todas las hojas de un archivo Excel (.xlsx), sin importar si están visibles u ocultas."""
118
+ try:
119
+ wb = load_workbook(RutaArchivo, read_only=True)
120
+ hojas = [sheet.title for sheet in wb.worksheets] # Todas las hojas
121
+ wb.close()
122
+ return hojas
123
+ except Exception as e:
124
+ print(f"Error al leer hojas de {RutaArchivo}: {e}")
125
+ return []
126
+
127
+ ################################################################################################################################
128
+ ################################################################################################################################
129
+ ################################## Retorna el color de las celdas de una hoja de un excel ###################################
130
+ ################################################################################################################################
131
+ ################################################################################################################################
132
+ def RetornarColoresCeldas(workbook, sheet, solo_coloreadas=True):
133
+ ws = workbook[sheet]
134
+ data = []
135
+ for fila in ws.iter_rows():
136
+ for celda in fila:
137
+
138
+ # Opcional: ignorar celdas totalmente vacías
139
+ if solo_coloreadas and celda.value is None:
140
+ continue
141
+
142
+ fill = celda.fill
143
+ color = "FFFFFF" # blanco por defecto
144
+
145
+ if fill and fill.patternType and fill.start_color.type == "rgb":
146
+ color = fill.start_color.rgb[-6:] # quitar alpha
147
+
148
+ data.append({
149
+ "fila": celda.row,
150
+ "columna": celda.column_letter,
151
+ "valor": celda.value,
152
+ "color_hex": color
153
+ })
154
+
155
+ return pd.DataFrame(data)
@@ -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)