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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64