1# pyright: reportMissingImports=false
2from __future__ import annotations
3
4from typing import TYPE_CHECKING
5
6from pandas.compat._optional import import_optional_dependency
7
8from pandas.io.excel._base import BaseExcelReader
9
10if TYPE_CHECKING:
11 from pyxlsb import Workbook
12
13 from pandas._typing import (
14 FilePath,
15 ReadBuffer,
16 Scalar,
17 StorageOptions,
18 )
19
20
21class PyxlsbReader(BaseExcelReader["Workbook"]):
22 def __init__(
23 self,
24 filepath_or_buffer: FilePath | ReadBuffer[bytes],
25 storage_options: StorageOptions | None = None,
26 engine_kwargs: dict | None = None,
27 ) -> None:
28 """
29 Reader using pyxlsb engine.
30
31 Parameters
32 ----------
33 filepath_or_buffer : str, path object, or Workbook
34 Object to be parsed.
35 storage_options : dict, optional
36 Extra options that make sense for a particular storage connection, e.g.
37 host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
38 are forwarded to ``urllib.request.Request`` as header options. For other
39 URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
40 forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
41 details, and for more examples on storage options refer `here
42 <https://pandas.pydata.org/docs/user_guide/io.html?
43 highlight=storage_options#reading-writing-remote-files>`_.
44 engine_kwargs : dict, optional
45 Arbitrary keyword arguments passed to excel engine.
46 """
47 import_optional_dependency("pyxlsb")
48 # This will call load_workbook on the filepath or buffer
49 # And set the result to the book-attribute
50 super().__init__(
51 filepath_or_buffer,
52 storage_options=storage_options,
53 engine_kwargs=engine_kwargs,
54 )
55
56 @property
57 def _workbook_class(self) -> type[Workbook]:
58 from pyxlsb import Workbook
59
60 return Workbook
61
62 def load_workbook(
63 self, filepath_or_buffer: FilePath | ReadBuffer[bytes], engine_kwargs
64 ) -> Workbook:
65 from pyxlsb import open_workbook
66
67 # TODO: hack in buffer capability
68 # This might need some modifications to the Pyxlsb library
69 # Actual work for opening it is in xlsbpackage.py, line 20-ish
70
71 return open_workbook(filepath_or_buffer, **engine_kwargs)
72
73 @property
74 def sheet_names(self) -> list[str]:
75 return self.book.sheets
76
77 def get_sheet_by_name(self, name: str):
78 self.raise_if_bad_sheet_by_name(name)
79 return self.book.get_sheet(name)
80
81 def get_sheet_by_index(self, index: int):
82 self.raise_if_bad_sheet_by_index(index)
83 # pyxlsb sheets are indexed from 1 onwards
84 # There's a fix for this in the source, but the pypi package doesn't have it
85 return self.book.get_sheet(index + 1)
86
87 def _convert_cell(self, cell) -> Scalar:
88 # TODO: there is no way to distinguish between floats and datetimes in pyxlsb
89 # This means that there is no way to read datetime types from an xlsb file yet
90 if cell.v is None:
91 return "" # Prevents non-named columns from not showing up as Unnamed: i
92 if isinstance(cell.v, float):
93 val = int(cell.v)
94 if val == cell.v:
95 return val
96 else:
97 return float(cell.v)
98
99 return cell.v
100
101 def get_sheet_data(
102 self,
103 sheet,
104 file_rows_needed: int | None = None,
105 ) -> list[list[Scalar]]:
106 data: list[list[Scalar]] = []
107 previous_row_number = -1
108 # When sparse=True the rows can have different lengths and empty rows are
109 # not returned. The cells are namedtuples of row, col, value (r, c, v).
110 for row in sheet.rows(sparse=True):
111 row_number = row[0].r
112 converted_row = [self._convert_cell(cell) for cell in row]
113 while converted_row and converted_row[-1] == "":
114 # trim trailing empty elements
115 converted_row.pop()
116 if converted_row:
117 data.extend([[]] * (row_number - previous_row_number - 1))
118 data.append(converted_row)
119 previous_row_number = row_number
120 if file_rows_needed is not None and len(data) >= file_rows_needed:
121 break
122 if data:
123 # extend rows to max_width
124 max_width = max(len(data_row) for data_row in data)
125 if min(len(data_row) for data_row in data) < max_width:
126 empty_cell: list[Scalar] = [""]
127 data = [
128 data_row + (max_width - len(data_row)) * empty_cell
129 for data_row in data
130 ]
131 return data