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.
- helpers/__init__.py +27 -0
- helpers/display_helper.py +135 -0
- helpers/table_manager.py +418 -0
- tablas_python/__init__.py +68 -0
- tablas_python/cli.py +49 -0
- tablas_python-0.1.0.dist-info/METADATA +375 -0
- tablas_python-0.1.0.dist-info/RECORD +22 -0
- tablas_python-0.1.0.dist-info/WHEEL +5 -0
- tablas_python-0.1.0.dist-info/entry_points.txt +3 -0
- tablas_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- tablas_python-0.1.0.dist-info/top_level.txt +3 -0
- utils/__init__.py +71 -0
- utils/batch_processor.py +111 -0
- utils/data_helpers.py +500 -0
- utils/excel_extractor.py +425 -0
- utils/excel_writer.py +188 -0
- utils/exporter.py +158 -0
- utils/file_utils.py +65 -0
- utils/pdf_extractor.py +129 -0
- utils/sqlite_extractor.py +133 -0
- utils/table_cleaner.py +246 -0
- utils/validator.py +155 -0
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
|