"""Lee un archivo Excel (.xlsx) a estructuras en memoria con openpyxl. Funcion impura: abre un libro Excel y devuelve sus hojas como listas de Python (headers + rows), no como markdown. Es el espejo en lectura de `write_xlsx_sheets`: lo que aquella escribe desde un dict {hoja: filas}, esta lo recupera al mismo shape. Maneja los tipos de celda nativos de Excel (fechas a ISO 8601, numeros int/float, bool, None) y lee el valor calculado de las formulas con data_only=True. No lanza: cualquier fallo (archivo inexistente, hoja inexistente, openpyxl ausente) se devuelve como dict {"status": "error", "error": "..."}. """ import datetime import os def read_xlsx( path: str, sheet: str = None, max_rows: int = None, header: bool = True, ) -> dict: """Lee un .xlsx a estructuras en memoria (headers + rows). Args: path: Ruta al archivo .xlsx a leer. sheet: Nombre de la hoja a leer. Si None (default) se leen TODAS las hojas del libro. max_rows: Maximo de filas a devolver por hoja (cuenta de filas de datos, sin contar la cabecera cuando header=True). None (default) = sin limite. header: Si True (default) la primera fila de cada hoja se interpreta como cabecera y va en "headers"; el resto va en "rows". Si False, no hay cabecera ("headers" es []) y todas las filas van en "rows". Returns: Dict. En exito: {"status": "ok", "sheets": {nombre_hoja: {"headers": [...], "rows": [[...], ...]}}} En error: {"status": "error", "error": ""}. Los valores de celda se devuelven como tipos nativos de Python: fechas/datetimes como str ISO 8601, int/float, bool, str y None. """ if not path: return {"status": "error", "error": "path no puede estar vacio"} abs_path = os.path.abspath(path) if not os.path.exists(abs_path): return {"status": "error", "error": f"archivo no encontrado: {abs_path}"} try: from openpyxl import load_workbook except ImportError: # pragma: no cover - dependencia del entorno return { "status": "error", "error": ( "openpyxl es requerido para read_xlsx. " "Instalar con: cd python && uv add openpyxl" ), } try: # data_only=True devuelve el valor calculado de las formulas (no la # formula). read_only acelera y reduce memoria en libros grandes. wb = load_workbook(abs_path, data_only=True, read_only=True) except Exception as exc: # noqa: BLE001 - el contrato del grupo es no lanzar return {"status": "error", "error": f"no se pudo abrir el libro: {exc}"} try: if sheet is not None: if sheet not in wb.sheetnames: return { "status": "error", "error": ( f"hoja '{sheet}' no existe. " f"Hojas disponibles: {wb.sheetnames}" ), } target = [sheet] else: target = list(wb.sheetnames) sheets = {} for name in target: ws = wb[name] sheets[name] = _read_sheet(ws, max_rows, header) return {"status": "ok", "sheets": sheets} finally: # En modo read_only conviene cerrar para liberar el archivo subyacente. wb.close() def _read_sheet(ws, max_rows, header) -> dict: """Lee una hoja a {"headers": [...], "rows": [[...]]} aplicando max_rows.""" headers = [] rows = [] first = True for raw_row in ws.iter_rows(values_only=True): row = [_coerce(v) for v in raw_row] if header and first: headers = row first = False continue first = False if max_rows is not None and len(rows) >= max_rows: break rows.append(row) return {"headers": headers, "rows": rows} def _coerce(value): """Convierte un valor de celda openpyxl a un tipo nativo de Python. Reglas: None se conserva; bool/int/float/str se conservan; fechas y datetimes se serializan a ISO 8601 (date a YYYY-MM-DD, datetime sin componente horario a YYYY-MM-DD, con hora a YYYY-MM-DDTHH:MM:SS); cualquier otro tipo se serializa a str. """ if value is None: return None # bool es subclase de int: comprobarlo antes que int. if isinstance(value, bool): return value if isinstance(value, (int, float, str)): return value if isinstance(value, datetime.datetime): if value.hour == 0 and value.minute == 0 and value.second == 0: return value.date().isoformat() return value.isoformat() if isinstance(value, datetime.date): return value.isoformat() return str(value) if __name__ == "__main__": # pragma: no cover - smoke manual import tempfile from openpyxl import Workbook tmp = os.path.join(tempfile.gettempdir(), "read_xlsx_demo.xlsx") wb = Workbook() ws = wb.active ws.title = "Ventas" ws.append(["Producto", "Unidades", "Precio", "Activo"]) ws.append(["Teclado", 12, 29.99, True]) ws.append(["Raton", 30, 14.5, False]) wb.save(tmp) print(read_xlsx(tmp)) print(read_xlsx(tmp, sheet="Ventas", max_rows=1))