tablas-python 0.1.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.
utils/exporter.py ADDED
@@ -0,0 +1,158 @@
1
+ """
2
+ Módulo dedicado a la exportación de DataFrames y lotes de tablas a archivos CSV y Excel.
3
+ Optimizado con codificación utf-8-sig y separadores configurables para máxima compatibilidad con Microsoft Excel.
4
+ """
5
+
6
+ from typing import Dict, List, Optional, Union, Any
7
+ import os
8
+ import pandas as pd
9
+ from .file_utils import ensure_dir, sanitize_filename
10
+
11
+
12
+ class TableExporter:
13
+ """
14
+ Clase de exportación para guardar DataFrames individuales o colecciones completas de tablas.
15
+ """
16
+
17
+ @staticmethod
18
+ def to_csv(
19
+ df: pd.DataFrame,
20
+ output_path: str,
21
+ sep: str = ";",
22
+ encoding: str = "utf-8-sig",
23
+ index: bool = False,
24
+ decimal: str = ",",
25
+ **kwargs
26
+ ) -> str:
27
+ """
28
+ Exporta un DataFrame a archivo CSV.
29
+
30
+ Parámetros:
31
+ -----------
32
+ df : pd.DataFrame
33
+ El DataFrame a exportar.
34
+ output_path : str
35
+ Ruta del archivo CSV destino.
36
+ sep : str (por defecto ';')
37
+ Separador de campos (';' abre perfectamente en Excel en español sin desfasar columnas).
38
+ encoding : str (por defecto 'utf-8-sig')
39
+ Codificación con BOM para que tildes, caracteres especiales y la 'ñ' se vean bien en Excel.
40
+ index : bool (por defecto False)
41
+ Si incluye la columna de índice numérico de pandas.
42
+ decimal : str (por defecto ',')
43
+ Separador decimal para valores numéricos en el CSV.
44
+
45
+ Retorna:
46
+ --------
47
+ str
48
+ Ruta absoluta del archivo CSV generado.
49
+ """
50
+ if not output_path.lower().endswith(".csv"):
51
+ output_path += ".csv"
52
+
53
+ ensure_dir(os.path.dirname(os.path.abspath(output_path)))
54
+
55
+ df.to_csv(
56
+ output_path,
57
+ sep=sep,
58
+ encoding=encoding,
59
+ index=index,
60
+ decimal=decimal,
61
+ **kwargs
62
+ )
63
+ return os.path.abspath(output_path)
64
+
65
+ @staticmethod
66
+ def to_excel(
67
+ df: pd.DataFrame,
68
+ output_path: str,
69
+ sheet_name: str = "Datos",
70
+ index: bool = False,
71
+ **kwargs
72
+ ) -> str:
73
+ """
74
+ Exporta un DataFrame a un archivo Excel (.xlsx).
75
+ """
76
+ if not output_path.lower().endswith((".xlsx", ".xls")):
77
+ output_path += ".xlsx"
78
+
79
+ ensure_dir(os.path.dirname(os.path.abspath(output_path)))
80
+
81
+ df.to_excel(
82
+ output_path,
83
+ sheet_name=sheet_name,
84
+ index=index,
85
+ engine="openpyxl",
86
+ **kwargs
87
+ )
88
+ return os.path.abspath(output_path)
89
+
90
+ @classmethod
91
+ def export_batch_to_csv(
92
+ cls,
93
+ tables_dict: Dict[str, pd.DataFrame],
94
+ output_dir: str,
95
+ sep: str = ";",
96
+ encoding: str = "utf-8-sig",
97
+ index: bool = False
98
+ ) -> List[str]:
99
+ """
100
+ Exporta múltiples DataFrames en archivos CSV individuales dentro de un directorio.
101
+
102
+ Parámetros:
103
+ -----------
104
+ tables_dict : dict
105
+ Diccionario {nombre_tabla: df} con los DataFrames a exportar.
106
+ output_dir : str
107
+ Directorio donde se guardarán los archivos CSV.
108
+
109
+ Retorna:
110
+ --------
111
+ List[str]
112
+ Lista con las rutas absolutas de todos los archivos CSV creados.
113
+ """
114
+ ensure_dir(output_dir)
115
+ generated_files: List[str] = []
116
+
117
+ for name, df in tables_dict.items():
118
+ if df is None or df.empty:
119
+ continue
120
+
121
+ clean_name = sanitize_filename(str(name))
122
+ file_name = f"{clean_name}.csv"
123
+ out_file = os.path.join(output_dir, file_name)
124
+
125
+ saved_path = cls.to_csv(
126
+ df=df,
127
+ output_path=out_file,
128
+ sep=sep,
129
+ encoding=encoding,
130
+ index=index
131
+ )
132
+ generated_files.append(saved_path)
133
+
134
+ return generated_files
135
+
136
+
137
+ # Funciones directas de conveniencia
138
+ def guardar_csv(
139
+ df: pd.DataFrame,
140
+ ruta: str,
141
+ sep: str = ";",
142
+ encoding: str = "utf-8-sig",
143
+ index: bool = False,
144
+ **kwargs
145
+ ) -> str:
146
+ """Función de una línea para guardar un DataFrame a CSV optimizado para Excel."""
147
+ return TableExporter.to_csv(df, output_path=ruta, sep=sep, encoding=encoding, index=index, **kwargs)
148
+
149
+
150
+ def guardar_excel(
151
+ df: pd.DataFrame,
152
+ ruta: str,
153
+ sheet_name: str = "Datos",
154
+ index: bool = False,
155
+ **kwargs
156
+ ) -> str:
157
+ """Función de una línea para guardar un DataFrame a Excel."""
158
+ return TableExporter.to_excel(df, output_path=ruta, sheet_name=sheet_name, index=index, **kwargs)
utils/file_utils.py ADDED
@@ -0,0 +1,65 @@
1
+ """
2
+ Módulo de utilidades para manejo seguro de rutas de archivos y nombres de exportación.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import re
8
+ from typing import Optional
9
+
10
+ # Configurar automáticamente la salida UTF-8 en Windows al importar
11
+ if sys.platform == "win32":
12
+ try:
13
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
14
+ except Exception:
15
+ pass
16
+
17
+
18
+ def resolve_file_path(file_path: str) -> str:
19
+ """
20
+ Resuelve una ruta de archivo ya sea que se pase:
21
+ - Ruta absoluta (ej: C:/Users/.../archivo.xlsx)
22
+ - Ruta relativa al directorio de trabajo actual (ej: samples/archivo.xlsx o ./archivo.xlsx)
23
+ - Nombre simple de archivo en la misma carpeta del script o en la carpeta samples.
24
+ """
25
+ if not file_path:
26
+ raise ValueError("La ruta del archivo no puede estar vacía.")
27
+
28
+ # 1. Si es absoluta y existe
29
+ if os.path.isabs(file_path) and os.path.exists(file_path):
30
+ return os.path.abspath(file_path)
31
+
32
+ # 2. Relativa directa al directorio de trabajo actual (CWD)
33
+ if os.path.exists(file_path):
34
+ return os.path.abspath(file_path)
35
+
36
+ # 3. Relativo al directorio raíz del proyecto
37
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38
+ alt_path = os.path.join(base_dir, file_path)
39
+ if os.path.exists(alt_path):
40
+ return os.path.abspath(alt_path)
41
+
42
+ # 4. Dentro de la carpeta samples
43
+ samples_path = os.path.join(base_dir, "samples", file_path)
44
+ if os.path.exists(samples_path):
45
+ return os.path.abspath(samples_path)
46
+
47
+ # Si no se encuentra, retornar la ruta absoluta esperada para que el error sea claro
48
+ return os.path.abspath(file_path)
49
+
50
+
51
+ def sanitize_filename(name: str, replacement: str = "_") -> str:
52
+ """
53
+ Limpia una cadena para que sea un nombre de archivo válido en Windows/Linux/Mac.
54
+ """
55
+ # Eliminar caracteres no permitidos en nombres de archivo
56
+ sanitized = re.sub(r'[\\/*?:"<>|]', replacement, name)
57
+ # Reducir espacios y guiones repetidos
58
+ sanitized = re.sub(r'\s+', '_', sanitized).strip('._ ')
59
+ return sanitized if sanitized else "tabla"
60
+
61
+
62
+ def ensure_dir(dir_path: str) -> str:
63
+ """Asegura que el directorio exista creándolo recursivamente si es necesario."""
64
+ os.makedirs(dir_path, exist_ok=True)
65
+ return os.path.abspath(dir_path)
utils/pdf_extractor.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ Módulo para extracción de tablas desde archivos PDF usando pdfplumber.
3
+ Soporta detección de múltiples tablas por página, configuraciones de extracción
4
+ y escaneo página por página o completo.
5
+ """
6
+
7
+ from typing import List, Dict, Any, Optional
8
+ from dataclasses import dataclass
9
+ import os
10
+ import pdfplumber
11
+ from .file_utils import resolve_file_path
12
+
13
+
14
+ @dataclass
15
+ class RawTableInfo:
16
+ """Información y contenido crudo de una tabla detectada."""
17
+ table_id: int
18
+ source_file: str
19
+ page_number: int
20
+ table_in_page: int
21
+ raw_data: List[List[Any]]
22
+ num_rows: int
23
+ num_cols: int
24
+ bbox: Optional[tuple] = None
25
+
26
+ def get_preview(self, max_rows: int = 5) -> List[List[Any]]:
27
+ """Retorna las primeras filas crudas para inspección visual."""
28
+ return self.raw_data[:max_rows]
29
+
30
+
31
+ class PDFTableExtractor:
32
+ """
33
+ Extractor de tablas especializado en documentos PDF.
34
+ """
35
+
36
+ def __init__(self, file_path: str):
37
+ self.file_path = resolve_file_path(file_path)
38
+ if not os.path.exists(self.file_path):
39
+ raise FileNotFoundError(f"El archivo PDF no existe: {self.file_path}")
40
+
41
+ def extract_all_tables(
42
+ self,
43
+ pages: Optional[List[int]] = None,
44
+ table_settings: Optional[Dict[str, Any]] = None,
45
+ fallback_text_strategy: bool = True
46
+ ) -> List[RawTableInfo]:
47
+ """
48
+ Extrae todas las tablas del PDF.
49
+
50
+ Parámetros:
51
+ -----------
52
+ pages : list of int, opcional
53
+ Lista de números de página a procesar (1-indexed). Si es None, procesa todo el PDF.
54
+ table_settings : dict, opcional
55
+ Configuración personalizada para pdfplumber (ej. vertical_strategy, horizontal_strategy).
56
+ fallback_text_strategy : bool (por defecto True)
57
+ Si con la estrategia estándar no se detectan tablas, intenta con estrategia de texto.
58
+
59
+ Retorna:
60
+ --------
61
+ List[RawTableInfo]
62
+ Lista con la información y datos crudos de cada tabla encontrada.
63
+ """
64
+ tables_found: List[RawTableInfo] = []
65
+ global_table_id = 1
66
+
67
+ with pdfplumber.open(self.file_path) as pdf:
68
+ total_pages = len(pdf.pages)
69
+ page_indices = range(total_pages)
70
+
71
+ if pages is not None:
72
+ # Convertir a 0-indexed y filtrar válidas
73
+ page_indices = [p - 1 for p in pages if 1 <= p <= total_pages]
74
+
75
+ for p_idx in page_indices:
76
+ page = pdf.pages[p_idx]
77
+ page_num = p_idx + 1
78
+
79
+ # 1. Intento con settings provistos o estándar
80
+ extracted = page.extract_tables(table_settings) if table_settings else page.extract_tables()
81
+
82
+ # 2. Si no encontró y fallback está activo, probar estrategia text
83
+ if not extracted and fallback_text_strategy:
84
+ alt_settings = {
85
+ "vertical_strategy": "text",
86
+ "horizontal_strategy": "text",
87
+ "snap_tolerance": 3,
88
+ }
89
+ extracted = page.extract_tables(alt_settings)
90
+
91
+ # Procesar cada tabla detectada en la página
92
+ for table_idx, raw_table in enumerate(extracted, start=1):
93
+ # Validar que la tabla tenga contenido útil
94
+ if not raw_table or len(raw_table) == 0:
95
+ continue
96
+
97
+ # Calcular filas y columnas
98
+ n_rows = len(raw_table)
99
+ n_cols = max((len(r) for r in raw_table if isinstance(r, list)), default=0)
100
+
101
+ info = RawTableInfo(
102
+ table_id=global_table_id,
103
+ source_file=self.file_path,
104
+ page_number=page_num,
105
+ table_in_page=table_idx,
106
+ raw_data=raw_table,
107
+ num_rows=n_rows,
108
+ num_cols=n_cols
109
+ )
110
+ tables_found.append(info)
111
+ global_table_id += 1
112
+
113
+ return tables_found
114
+
115
+ def extract_table_by_id(
116
+ self,
117
+ table_id: int,
118
+ table_settings: Optional[Dict[str, Any]] = None
119
+ ) -> RawTableInfo:
120
+ """
121
+ Extrae y devuelve directamente la tabla con el ID global indicado (1-indexed).
122
+ """
123
+ all_tables = self.extract_all_tables(table_settings=table_settings)
124
+ for t in all_tables:
125
+ if t.table_id == table_id:
126
+ return t
127
+ raise IndexError(
128
+ f"No se encontró la tabla {table_id}. Tablas disponibles: {len(all_tables)}"
129
+ )
@@ -0,0 +1,133 @@
1
+ """
2
+ Módulo para extracción de tablas y consultas desde bases de datos SQLite (.db, .sqlite, .sqlite3).
3
+ Permite listar tablas, inspeccionar esquemas y extraer DataFrames de tablas completas o consultas SQL.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional, Union
7
+ from dataclasses import dataclass
8
+ import os
9
+ import sqlite3
10
+ import pandas as pd
11
+ from .file_utils import resolve_file_path
12
+
13
+
14
+ @dataclass
15
+ class RawSQLiteTableInfo:
16
+ """Información de una tabla o vista detectada en una base de datos SQLite."""
17
+ table_id: int
18
+ source_file: str
19
+ table_name: str
20
+ table_type: str # 'table' o 'view'
21
+ columns: List[str]
22
+ num_rows: int
23
+ raw_data: List[List[Any]]
24
+ num_cols: int
25
+
26
+ def get_preview(self, max_rows: int = 5) -> List[List[Any]]:
27
+ """Retorna los encabezados y las primeras filas de datos."""
28
+ if not self.raw_data:
29
+ return [self.columns]
30
+ return [self.columns] + self.raw_data[:max_rows]
31
+
32
+
33
+ class SQLiteTableExtractor:
34
+ """
35
+ Extractor de datos para bases de datos SQLite.
36
+ """
37
+
38
+ def __init__(self, file_path: str):
39
+ self.file_path = resolve_file_path(file_path)
40
+ if not os.path.exists(self.file_path):
41
+ raise FileNotFoundError(f"No se encontró la base de datos SQLite en: {self.file_path}")
42
+
43
+ def list_tables(self) -> List[Dict[str, str]]:
44
+ """Lista todas las tablas y vistas disponibles en la base de datos."""
45
+ with sqlite3.connect(self.file_path) as conn:
46
+ cursor = conn.cursor()
47
+ cursor.execute(
48
+ "SELECT name, type FROM sqlite_master "
49
+ "WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' "
50
+ "ORDER BY name"
51
+ )
52
+ rows = cursor.fetchall()
53
+ return [{"name": r[0], "type": r[1]} for r in rows]
54
+
55
+ def extract_all_tables(self) -> List[RawSQLiteTableInfo]:
56
+ """
57
+ Extrae la información y una muestra de datos de todas las tablas en la base de datos.
58
+ """
59
+ tables = self.list_tables()
60
+ tables_found: List[RawSQLiteTableInfo] = []
61
+
62
+ with sqlite3.connect(self.file_path) as conn:
63
+ for idx, item in enumerate(tables, start=1):
64
+ t_name = item["name"]
65
+ t_type = item["type"]
66
+
67
+ # Obtener columnas
68
+ cursor = conn.cursor()
69
+ cursor.execute(f"PRAGMA table_info('{t_name}')")
70
+ col_info = cursor.fetchall()
71
+ cols = [c[1] for c in col_info] if col_info else []
72
+
73
+ # Obtener conteo de filas
74
+ try:
75
+ cursor.execute(f"SELECT COUNT(*) FROM '{t_name}'")
76
+ row_count = cursor.fetchone()[0]
77
+ except Exception:
78
+ row_count = 0
79
+
80
+ # Obtener primeras filas
81
+ cursor.execute(f"SELECT * FROM '{t_name}' LIMIT 20")
82
+ sample_data = [list(r) for r in cursor.fetchall()]
83
+
84
+ info = RawSQLiteTableInfo(
85
+ table_id=idx,
86
+ source_file=self.file_path,
87
+ table_name=t_name,
88
+ table_type=t_type,
89
+ columns=cols,
90
+ num_rows=row_count,
91
+ raw_data=sample_data,
92
+ num_cols=len(cols)
93
+ )
94
+ tables_found.append(info)
95
+
96
+ return tables_found
97
+
98
+ def get_table_df(self, table_name_or_id: Union[str, int]) -> pd.DataFrame:
99
+ """
100
+ Obtiene un DataFrame con la tabla completa por su nombre o su ID (1-indexed).
101
+ """
102
+ tables = self.list_tables()
103
+ selected_name = None
104
+
105
+ if isinstance(table_name_or_id, int):
106
+ if 1 <= table_name_or_id <= len(tables):
107
+ selected_name = tables[table_name_or_id - 1]["name"]
108
+ else:
109
+ raise IndexError(f"ID de tabla {table_name_or_id} fuera de rango. Hay {len(tables)} tablas.")
110
+ else:
111
+ table_str = str(table_name_or_id).strip()
112
+ # Buscar coincidencia exacta o insensible a mayúsculas
113
+ for t in tables:
114
+ if t["name"].lower() == table_str.lower():
115
+ selected_name = t["name"]
116
+ break
117
+ if not selected_name:
118
+ raise ValueError(f"No se encontró la tabla '{table_name_or_id}' en {self.file_path}")
119
+
120
+ with sqlite3.connect(self.file_path) as conn:
121
+ df = pd.read_sql_query(f"SELECT * FROM '{selected_name}'", conn)
122
+ return df
123
+
124
+ def query_df(self, sql_query: str, params: Optional[Union[tuple, dict]] = None) -> pd.DataFrame:
125
+ """
126
+ Ejecuta una consulta SQL personalizada y devuelve el resultado en un DataFrame.
127
+ """
128
+ with sqlite3.connect(self.file_path) as conn:
129
+ if params is not None:
130
+ df = pd.read_sql_query(sql_query, conn, params=params)
131
+ else:
132
+ df = pd.read_sql_query(sql_query, conn)
133
+ return df