excelreader-native 2.1.1__py3-none-win_amd64.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.
- excelreader/__init__.py +13 -0
- excelreader/_lib/.gitkeep +0 -0
- excelreader/_lib/ExcelReader.Native.dll +0 -0
- excelreader/_native.py +116 -0
- excelreader/py.typed +0 -0
- excelreader/reader.py +190 -0
- excelreader/types.py +39 -0
- excelreader_native-2.1.1.dist-info/METADATA +87 -0
- excelreader_native-2.1.1.dist-info/RECORD +10 -0
- excelreader_native-2.1.1.dist-info/WHEEL +4 -0
excelreader/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Python bindings for ExcelReader. Reading only — writing is not exposed yet."""
|
|
2
|
+
|
|
3
|
+
from excelreader.reader import Workbook, open_bytes, open_workbook
|
|
4
|
+
from excelreader.types import Cell, CellType, ExcelReaderError
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Cell",
|
|
8
|
+
"CellType",
|
|
9
|
+
"ExcelReaderError",
|
|
10
|
+
"Workbook",
|
|
11
|
+
"open_bytes",
|
|
12
|
+
"open_workbook",
|
|
13
|
+
]
|
|
File without changes
|
|
Binary file
|
excelreader/_native.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""ctypes binding to the ExcelReader NativeAOT shared library.
|
|
2
|
+
|
|
3
|
+
Everything here mirrors src/ExcelReader.Native/include/excelreader.h. If you change one, change both.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ctypes
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
XL_OK = 0
|
|
15
|
+
XL_EOF = -1
|
|
16
|
+
XL_BUFFER_TOO_SMALL = -2
|
|
17
|
+
XL_INVALID_HANDLE = -3
|
|
18
|
+
XL_INVALID_ARGUMENT = -4
|
|
19
|
+
XL_ERROR = -5
|
|
20
|
+
|
|
21
|
+
XL_FORMAT_AUTO = 0
|
|
22
|
+
XL_FORMAT_XLS = 1
|
|
23
|
+
XL_FORMAT_XLSX = 2
|
|
24
|
+
XL_FORMAT_XLSB = 3
|
|
25
|
+
XL_FORMAT_CSV = 4
|
|
26
|
+
|
|
27
|
+
class NativeRowCell(ctypes.Structure):
|
|
28
|
+
_fields_ = [
|
|
29
|
+
("column", ctypes.c_int32),
|
|
30
|
+
("type", ctypes.c_int32),
|
|
31
|
+
("value_len", ctypes.c_int32),
|
|
32
|
+
("value", ctypes.POINTER(ctypes.c_uint8)),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NativeRow(ctypes.Structure):
|
|
37
|
+
_fields_ = [
|
|
38
|
+
("cell_count", ctypes.c_int32),
|
|
39
|
+
("cells", ctypes.POINTER(NativeRowCell)),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NativeRows(ctypes.Structure):
|
|
44
|
+
_fields_ = [
|
|
45
|
+
("row_count", ctypes.c_int32),
|
|
46
|
+
("rows", ctypes.POINTER(NativeRow)),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_LIB_NAMES = {
|
|
51
|
+
"Windows": "ExcelReader.Native.dll",
|
|
52
|
+
"Linux": "ExcelReader.Native.so",
|
|
53
|
+
"Darwin": "ExcelReader.Native.dylib",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# NativeAOT emits no "lib" prefix, so the filename is the assembly name on every platform.
|
|
57
|
+
def library_filename() -> str:
|
|
58
|
+
try:
|
|
59
|
+
return _LIB_NAMES[platform.system()]
|
|
60
|
+
except KeyError:
|
|
61
|
+
raise RuntimeError(f"unsupported platform: {platform.system()}") from None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _candidate_paths() -> list[Path]:
|
|
65
|
+
override = os.environ.get("EXCELREADER_NATIVE_LIB")
|
|
66
|
+
if override:
|
|
67
|
+
return [Path(override)]
|
|
68
|
+
return [Path(__file__).resolve().parent / "_lib" / library_filename()]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@lru_cache(maxsize=1)
|
|
72
|
+
def load_library() -> ctypes.CDLL:
|
|
73
|
+
for path in _candidate_paths():
|
|
74
|
+
if path.exists():
|
|
75
|
+
return _bind(ctypes.CDLL(str(path)))
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
f"{library_filename()} not found. Build it with:\n"
|
|
78
|
+
f" python python/scripts/build_native.py\n"
|
|
79
|
+
f"or point EXCELREADER_NATIVE_LIB at an existing binary."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _bind(lib: ctypes.CDLL) -> ctypes.CDLL:
|
|
84
|
+
c_int = ctypes.c_int32
|
|
85
|
+
p_int = ctypes.POINTER(ctypes.c_int32)
|
|
86
|
+
p_void = ctypes.c_void_p
|
|
87
|
+
pp_void = ctypes.POINTER(ctypes.c_void_p)
|
|
88
|
+
p_bytes = ctypes.c_char_p
|
|
89
|
+
|
|
90
|
+
lib.xl_open_file.argtypes = [p_bytes, c_int, c_int, pp_void]
|
|
91
|
+
lib.xl_open_file.restype = c_int
|
|
92
|
+
lib.xl_open_memory.argtypes = [p_bytes, c_int, c_int, pp_void]
|
|
93
|
+
lib.xl_open_memory.restype = c_int
|
|
94
|
+
lib.xl_close.argtypes = [p_void]
|
|
95
|
+
lib.xl_close.restype = c_int
|
|
96
|
+
lib.xl_sheet_count.argtypes = [p_void, p_int]
|
|
97
|
+
lib.xl_sheet_count.restype = c_int
|
|
98
|
+
# xl_sheet_name / xl_next_row / xl_last_error write INTO their buffer argument.
|
|
99
|
+
# Callers MUST pass ctypes.create_string_buffer(n), never a bytes literal — bytes
|
|
100
|
+
# objects are immutable/interned in CPython, and letting native code write into
|
|
101
|
+
# one is undefined behavior.
|
|
102
|
+
lib.xl_sheet_name.argtypes = [p_void, p_bytes, c_int, p_int]
|
|
103
|
+
lib.xl_sheet_name.restype = c_int
|
|
104
|
+
lib.xl_move_to_sheet.argtypes = [p_void, c_int]
|
|
105
|
+
lib.xl_move_to_sheet.restype = c_int
|
|
106
|
+
lib.xl_is_date1904.argtypes = [p_void, p_int]
|
|
107
|
+
lib.xl_is_date1904.restype = c_int
|
|
108
|
+
lib.xl_next_row.argtypes = [p_void, p_bytes, c_int, p_int]
|
|
109
|
+
lib.xl_next_row.restype = c_int
|
|
110
|
+
lib.xl_last_error.argtypes = [p_bytes, c_int, p_int]
|
|
111
|
+
lib.xl_last_error.restype = c_int
|
|
112
|
+
lib.xl_read_all_decoded.argtypes = [p_void, ctypes.POINTER(NativeRows)]
|
|
113
|
+
lib.xl_read_all_decoded.restype = c_int
|
|
114
|
+
lib.xl_free_rows.argtypes = [ctypes.POINTER(NativeRows)]
|
|
115
|
+
lib.xl_free_rows.restype = None
|
|
116
|
+
return lib
|
excelreader/py.typed
ADDED
|
File without changes
|
excelreader/reader.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""The public reading API. One Workbook wraps one native handle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import struct
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
from excelreader import _native
|
|
13
|
+
from excelreader.types import Cell, CellType, ExcelReaderError
|
|
14
|
+
|
|
15
|
+
_FORMATS = {
|
|
16
|
+
"auto": _native.XL_FORMAT_AUTO,
|
|
17
|
+
"xls": _native.XL_FORMAT_XLS,
|
|
18
|
+
"xlsx": _native.XL_FORMAT_XLSX,
|
|
19
|
+
"xlsb": _native.XL_FORMAT_XLSB,
|
|
20
|
+
"csv": _native.XL_FORMAT_CSV,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_CELL_HEADER = struct.Struct("<iii")
|
|
24
|
+
_INITIAL_ROW_BUFFER = 64 * 1024
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _last_error() -> str:
|
|
28
|
+
lib = _native.load_library()
|
|
29
|
+
length = ctypes.c_int32()
|
|
30
|
+
buffer = ctypes.create_string_buffer(1024)
|
|
31
|
+
if lib.xl_last_error(buffer, len(buffer), ctypes.byref(length)) == _native.XL_BUFFER_TOO_SMALL:
|
|
32
|
+
buffer = ctypes.create_string_buffer(length.value)
|
|
33
|
+
lib.xl_last_error(buffer, len(buffer), ctypes.byref(length))
|
|
34
|
+
return buffer.raw[: length.value].decode("utf-8", errors="replace")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check(status: int) -> None:
|
|
38
|
+
if status == _native.XL_OK:
|
|
39
|
+
return
|
|
40
|
+
if status == _native.XL_INVALID_HANDLE:
|
|
41
|
+
raise ExcelReaderError("workbook is closed or the handle is invalid")
|
|
42
|
+
if status == _native.XL_INVALID_ARGUMENT:
|
|
43
|
+
raise ExcelReaderError("invalid argument passed to the native library")
|
|
44
|
+
raise ExcelReaderError(_last_error() or f"native call failed with status {status}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_format(name: str | None, path: Path | None) -> int:
|
|
48
|
+
if name is not None:
|
|
49
|
+
try:
|
|
50
|
+
return _FORMATS[name.lower()]
|
|
51
|
+
except KeyError:
|
|
52
|
+
raise ValueError(f"unknown format {name!r}; expected one of {sorted(_FORMATS)}") from None
|
|
53
|
+
# The signature sniffer covers XLS/XLSX/XLSB but CSV has no signature, so the extension is the
|
|
54
|
+
# only hint available for it.
|
|
55
|
+
if path is not None and path.suffix.lower() == ".csv":
|
|
56
|
+
return _native.XL_FORMAT_CSV
|
|
57
|
+
return _native.XL_FORMAT_AUTO
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Workbook:
|
|
61
|
+
"""A read cursor over one workbook. Not thread-safe; use one instance per thread."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, handle: ctypes.c_void_p) -> None:
|
|
64
|
+
self._lib = _native.load_library()
|
|
65
|
+
self._handle: ctypes.c_void_p | None = handle
|
|
66
|
+
|
|
67
|
+
def _require_handle(self) -> ctypes.c_void_p:
|
|
68
|
+
if self._handle is None:
|
|
69
|
+
raise ExcelReaderError("workbook is closed")
|
|
70
|
+
return self._handle
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def sheet_count(self) -> int:
|
|
74
|
+
count = ctypes.c_int32()
|
|
75
|
+
_check(self._lib.xl_sheet_count(self._require_handle(), ctypes.byref(count)))
|
|
76
|
+
return count.value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def sheet_name(self) -> str:
|
|
80
|
+
handle = self._require_handle()
|
|
81
|
+
length = ctypes.c_int32()
|
|
82
|
+
buffer = ctypes.create_string_buffer(256)
|
|
83
|
+
status = self._lib.xl_sheet_name(handle, buffer, len(buffer), ctypes.byref(length))
|
|
84
|
+
if status == _native.XL_BUFFER_TOO_SMALL:
|
|
85
|
+
buffer = ctypes.create_string_buffer(length.value)
|
|
86
|
+
status = self._lib.xl_sheet_name(handle, buffer, len(buffer), ctypes.byref(length))
|
|
87
|
+
_check(status)
|
|
88
|
+
return buffer.raw[: length.value].decode("utf-8")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def is_date1904(self) -> bool:
|
|
92
|
+
flag = ctypes.c_int32()
|
|
93
|
+
_check(self._lib.xl_is_date1904(self._require_handle(), ctypes.byref(flag)))
|
|
94
|
+
return flag.value != 0
|
|
95
|
+
|
|
96
|
+
def move_to_sheet(self, index: int) -> None:
|
|
97
|
+
"""Selects a sheet and restarts row enumeration from its first row."""
|
|
98
|
+
_check(self._lib.xl_move_to_sheet(self._require_handle(), index))
|
|
99
|
+
|
|
100
|
+
def rows(self) -> Iterator[list[Cell]]:
|
|
101
|
+
written = ctypes.c_int32()
|
|
102
|
+
capacity = _INITIAL_ROW_BUFFER
|
|
103
|
+
buffer = ctypes.create_string_buffer(capacity)
|
|
104
|
+
while True:
|
|
105
|
+
handle = self._require_handle()
|
|
106
|
+
status = self._lib.xl_next_row(handle, buffer, capacity, ctypes.byref(written))
|
|
107
|
+
if status == _native.XL_EOF:
|
|
108
|
+
return
|
|
109
|
+
if status == _native.XL_BUFFER_TOO_SMALL:
|
|
110
|
+
# The native side holds the row until it fits, so growing loses nothing.
|
|
111
|
+
capacity = written.value
|
|
112
|
+
buffer = ctypes.create_string_buffer(capacity)
|
|
113
|
+
continue
|
|
114
|
+
_check(status)
|
|
115
|
+
yield _decode_row(buffer.raw, written.value)
|
|
116
|
+
|
|
117
|
+
def read_all(self) -> list[list[Cell]]:
|
|
118
|
+
"""Materializes every remaining row of the current sheet in one native call."""
|
|
119
|
+
handle = self._require_handle()
|
|
120
|
+
rows = _native.NativeRows()
|
|
121
|
+
_check(self._lib.xl_read_all_decoded(handle, ctypes.byref(rows)))
|
|
122
|
+
try:
|
|
123
|
+
return [_decode_native_row(rows.rows[index]) for index in range(rows.row_count)]
|
|
124
|
+
finally:
|
|
125
|
+
self._lib.xl_free_rows(ctypes.byref(rows))
|
|
126
|
+
|
|
127
|
+
def close(self) -> None:
|
|
128
|
+
if self._handle is None:
|
|
129
|
+
return
|
|
130
|
+
handle, self._handle = self._handle, None
|
|
131
|
+
_check(self._lib.xl_close(handle))
|
|
132
|
+
|
|
133
|
+
def __enter__(self) -> Self:
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __exit__(self, *_exc_info: object) -> None:
|
|
137
|
+
self.close()
|
|
138
|
+
|
|
139
|
+
def __del__(self) -> None:
|
|
140
|
+
# Backstop only, not a substitute for explicit close()/`with`: if a Workbook is dropped
|
|
141
|
+
# without one, this still releases the native handle and the file lock it holds. During
|
|
142
|
+
# interpreter shutdown or GC, module globals (_native, ctypes) may already be partially torn
|
|
143
|
+
# down, so a finalizer must never let an exception escape — swallow anything broadly here,
|
|
144
|
+
# which is the standard, accepted exception to "never bare-except" for __del__ specifically.
|
|
145
|
+
try:
|
|
146
|
+
self.close()
|
|
147
|
+
except Exception: # noqa: BLE001, S110
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _decode_row(blob: bytes, length: int) -> list[Cell]:
|
|
152
|
+
count = struct.unpack_from("<i", blob, 0)[0]
|
|
153
|
+
cells: list[Cell] = []
|
|
154
|
+
offset = 4
|
|
155
|
+
for _ in range(count):
|
|
156
|
+
column, cell_type, value_length = _CELL_HEADER.unpack_from(blob, offset)
|
|
157
|
+
offset += _CELL_HEADER.size
|
|
158
|
+
value = blob[offset : offset + value_length].decode("utf-8")
|
|
159
|
+
offset += value_length
|
|
160
|
+
cells.append(Cell(column=column, type=CellType(cell_type), value=value))
|
|
161
|
+
if offset != length:
|
|
162
|
+
raise ExcelReaderError(f"row blob is malformed: consumed {offset} of {length} bytes")
|
|
163
|
+
return cells
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _decode_native_row(row: _native.NativeRow) -> list[Cell]:
|
|
167
|
+
cells: list[Cell] = []
|
|
168
|
+
for index in range(row.cell_count):
|
|
169
|
+
cell = row.cells[index]
|
|
170
|
+
value = ctypes.string_at(cell.value, cell.value_len).decode("utf-8")
|
|
171
|
+
cells.append(Cell(column=cell.column, type=CellType(cell.type), value=value))
|
|
172
|
+
return cells
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def open_workbook(path: str | Path, format: str | None = None) -> Workbook:
|
|
176
|
+
"""Opens a workbook from disk. `format` is one of auto/xls/xlsx/xlsb/csv; None infers it."""
|
|
177
|
+
resolved = Path(path)
|
|
178
|
+
encoded = str(resolved).encode("utf-8")
|
|
179
|
+
handle = ctypes.c_void_p()
|
|
180
|
+
lib = _native.load_library()
|
|
181
|
+
_check(lib.xl_open_file(encoded, len(encoded), _resolve_format(format, resolved), ctypes.byref(handle)))
|
|
182
|
+
return Workbook(handle)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def open_bytes(data: bytes, format: str | None = None) -> Workbook:
|
|
186
|
+
"""Opens a workbook from an in-memory buffer. The native side copies `data` immediately."""
|
|
187
|
+
handle = ctypes.c_void_p()
|
|
188
|
+
lib = _native.load_library()
|
|
189
|
+
_check(lib.xl_open_memory(data, len(data), _resolve_format(format, None), ctypes.byref(handle)))
|
|
190
|
+
return Workbook(handle)
|
excelreader/types.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Value types crossing the FFI boundary. Mirrors ExcelReader.Core.Enums.CellType."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import NamedTuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CellType(IntEnum):
|
|
11
|
+
EMPTY = 0
|
|
12
|
+
STRING = 1
|
|
13
|
+
NUMBER = 2
|
|
14
|
+
DATE = 3
|
|
15
|
+
BOOL = 4
|
|
16
|
+
FORMULA = 5
|
|
17
|
+
ERROR = 6
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Cell(NamedTuple):
|
|
21
|
+
"""One cell. `value` is the raw text as stored, so DATE cells are Excel serial numbers."""
|
|
22
|
+
|
|
23
|
+
column: int
|
|
24
|
+
type: CellType
|
|
25
|
+
value: str
|
|
26
|
+
|
|
27
|
+
def as_date(self, date1904: bool = False) -> datetime.date | None:
|
|
28
|
+
"""Converts a DATE cell's serial value to a date. Returns None for any other cell type.
|
|
29
|
+
|
|
30
|
+
`date1904` should come from `Workbook.is_date1904` for the workbook this cell came from.
|
|
31
|
+
"""
|
|
32
|
+
if self.type is not CellType.DATE:
|
|
33
|
+
return None
|
|
34
|
+
epoch = datetime.date(1904, 1, 1) if date1904 else datetime.date(1899, 12, 30)
|
|
35
|
+
return epoch + datetime.timedelta(days=int(float(self.value)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExcelReaderError(Exception):
|
|
39
|
+
"""Raised when the native library reports a failure."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: excelreader-native
|
|
3
|
+
Version: 2.1.1
|
|
4
|
+
Summary: Read XLSX, XLSB, XLS and CSV through the ExcelReader NativeAOT library
|
|
5
|
+
Project-URL: Homepage, https://github.com/GabrielMarquezMatte/ExcelReader
|
|
6
|
+
Author: Gabriel Matte
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: typing-extensions>=4.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# excelreader (Python)
|
|
17
|
+
|
|
18
|
+
Read XLSX, XLSB, XLS and CSV through ExcelReader's NativeAOT library. No .NET runtime required —
|
|
19
|
+
the shared library is self-contained. Reading only; writing is not exposed yet.
|
|
20
|
+
|
|
21
|
+
## Install (from source)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python python/scripts/build_native.py # requires the .NET 10 SDK, once per machine
|
|
25
|
+
pip install -e "python[dev]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`build_native.py` publishes `src/ExcelReader.Native` for your platform and copies the resulting
|
|
29
|
+
`ExcelReader.Native.{dll,so,dylib}` into `excelreader/_lib/`. To point at a binary you built
|
|
30
|
+
elsewhere, set `EXCELREADER_NATIVE_LIB` to its full path.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from excelreader import open_workbook
|
|
36
|
+
|
|
37
|
+
with open_workbook("book.xlsx") as workbook:
|
|
38
|
+
print(workbook.sheet_count, workbook.sheet_name)
|
|
39
|
+
for row in workbook.rows():
|
|
40
|
+
for cell in row:
|
|
41
|
+
print(cell.column, cell.type.name, cell.value)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Formats
|
|
45
|
+
|
|
46
|
+
`open_workbook` sniffs XLS/XLSX/XLSB by file signature. CSV has no signature, so it is chosen by the
|
|
47
|
+
`.csv` extension — or explicitly:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
open_workbook("data.txt", format="csv")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Dates
|
|
54
|
+
|
|
55
|
+
`cell.value` is always the raw text as stored, so `CellType.DATE` cells hold Excel serial numbers.
|
|
56
|
+
Use `Cell.as_date()` to convert, passing the workbook's epoch flag:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
as_date = cell.as_date(workbook.is_date1904)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`as_date()` returns `None` for any cell that isn't `CellType.DATE`.
|
|
63
|
+
|
|
64
|
+
### Reading everything at once
|
|
65
|
+
|
|
66
|
+
`rows()` iterates row-by-row; `read_all()` materializes the whole sheet in one call:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
all_rows = workbook.read_all()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This holds every row in memory at once, so prefer `rows()` for very large sheets.
|
|
73
|
+
|
|
74
|
+
### From memory
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from excelreader import open_bytes
|
|
78
|
+
|
|
79
|
+
with open_bytes(payload) as workbook:
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- A `Workbook` is **not** thread-safe. Use one per thread.
|
|
86
|
+
- Empty cells are skipped, so `cell.column` may skip indices. Do not assume `row[i].column == i`.
|
|
87
|
+
- The ABI is documented in `src/ExcelReader.Native/include/excelreader.h`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
excelreader/__init__.py,sha256=Env8EiLc1AZbZrzm9b-LXcvsFX8pZaThOYVGSjnDwEk,337
|
|
2
|
+
excelreader/_native.py,sha256=m1Cc9EhLNd-m-DXHsRcPXMHoXoNaiJGxWs0lstVTAiE,3637
|
|
3
|
+
excelreader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
excelreader/reader.py,sha256=B6vJZAN4lubsIvouIpSgavSWsXxZAaT7F6w0LkMvuI8,7467
|
|
5
|
+
excelreader/types.py,sha256=Xa8CoIq2U7N-0DM2GwWRIxKm1-N0rNR9aNQg11zxvbI,1093
|
|
6
|
+
excelreader/_lib/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
excelreader/_lib/ExcelReader.Native.dll,sha256=H28ji4Mk_aR8zq30DVxjeXVb8i1IAsaHoZG60uiBh1c,2506752
|
|
8
|
+
excelreader_native-2.1.1.dist-info/METADATA,sha256=QTgO1doyB7rvj2_NHBm4m-jJfek1BKi2lXiDvm0dBBw,2515
|
|
9
|
+
excelreader_native-2.1.1.dist-info/WHEEL,sha256=OA-gEgWbLnh0Tf6JrLOMFR4vOWY4QbBMSZuN6osy-Q0,94
|
|
10
|
+
excelreader_native-2.1.1.dist-info/RECORD,,
|