1from __future__ import annotations
2
3from datetime import (
4 date,
5 datetime,
6 time,
7 timedelta,
8)
9from typing import (
10 TYPE_CHECKING,
11 Any,
12 TypeAlias,
13)
14
15from pandas.compat._optional import import_optional_dependency
16
17from pandas.io.excel._base import BaseExcelReader
18
19if TYPE_CHECKING:
20 from python_calamine import (
21 CalamineSheet,
22 CalamineWorkbook,
23 )
24
25 from pandas._typing import (
26 FilePath,
27 NaTType,
28 ReadBuffer,
29 Scalar,
30 StorageOptions,
31 )
32
33_CellValue: TypeAlias = int | float | str | bool | time | date | datetime | timedelta
34
35
36class CalamineReader(BaseExcelReader["CalamineWorkbook"]):
37 def __init__(
38 self,
39 filepath_or_buffer: FilePath | ReadBuffer[bytes],
40 storage_options: StorageOptions | None = None,
41 engine_kwargs: dict | None = None,
42 ) -> None:
43 """
44 Reader using calamine engine (xlsx/xls/xlsb/ods).
45
46 Parameters
47 ----------
48 filepath_or_buffer : str, path to be parsed or
49 an open readable stream.
50 storage_options : dict, optional
51 Extra options that make sense for a particular storage connection, e.g.
52 host, port, username, password, etc. For HTTP(S) URLs the key-value pairs
53 are forwarded to ``urllib.request.Request`` as header options. For other
54 URLs (e.g. starting with "s3://", and "gcs://") the key-value pairs are
55 forwarded to ``fsspec.open``. Please see ``fsspec`` and ``urllib`` for more
56 details, and for more examples on storage options refer `here
57 <https://pandas.pydata.org/docs/user_guide/io.html?
58 highlight=storage_options#reading-writing-remote-files>`_.
59 engine_kwargs : dict, optional
60 Arbitrary keyword arguments passed to excel engine.
61 """
62 import_optional_dependency("python_calamine")
63 super().__init__(
64 filepath_or_buffer,
65 storage_options=storage_options,
66 engine_kwargs=engine_kwargs,
67 )
68
69 @property
70 def _workbook_class(self) -> type[CalamineWorkbook]:
71 from python_calamine import CalamineWorkbook
72
73 return CalamineWorkbook
74
75 def load_workbook(
76 self, filepath_or_buffer: FilePath | ReadBuffer[bytes], engine_kwargs: Any
77 ) -> CalamineWorkbook:
78 from python_calamine import load_workbook
79
80 return load_workbook(
81 filepath_or_buffer,
82 **engine_kwargs,
83 )
84
85 @property
86 def sheet_names(self) -> list[str]:
87 from python_calamine import SheetTypeEnum
88
89 return [
90 sheet.name
91 for sheet in self.book.sheets_metadata
92 if sheet.typ == SheetTypeEnum.WorkSheet
93 ]
94
95 def get_sheet_by_name(self, name: str) -> CalamineSheet:
96 self.raise_if_bad_sheet_by_name(name)
97 return self.book.get_sheet_by_name(name)
98
99 def get_sheet_by_index(self, index: int) -> CalamineSheet:
100 self.raise_if_bad_sheet_by_index(index)
101 return self.book.get_sheet_by_index(index)
102
103 def get_sheet_data(
104 self, sheet: CalamineSheet, file_rows_needed: int | None = None
105 ) -> list[list[Scalar | NaTType | time]]:
106 def _convert_cell(value: _CellValue) -> Scalar | NaTType | time:
107 if isinstance(value, float):
108 val = int(value)
109 if val == value:
110 return val
111 else:
112 return value
113 elif isinstance(value, (datetime, timedelta)):
114 # Return as-is to match openpyxl behavior (GH#59186)
115 return value
116 elif isinstance(value, date):
117 # Convert date to datetime to match openpyxl behavior (GH#59186)
118 return datetime(value.year, value.month, value.day)
119 elif isinstance(value, time):
120 return value
121
122 return value
123
124 rows: list[list[_CellValue]] = sheet.to_python(
125 skip_empty_area=False, nrows=file_rows_needed
126 )
127 data = [[_convert_cell(cell) for cell in row] for row in rows]
128
129 return data