excelizarr 0.1.0__py3-none-any.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.
excelizarr/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """A cloud-native representation of an ``.xlsx`` workbook, in Zarr.
2
+
3
+ A workbook becomes a zarr group, each sheet a group of aligned, chunked, typed 2D
4
+ arrays. Read a range straight off object storage, ``pd.read_excel`` a store without
5
+ unzipping any XML, and convert back to ``.xlsx`` with formulas and number formats
6
+ intact::
7
+
8
+ import excelizarr as xz
9
+
10
+ wb = xz.xlsx_to_zarr("model.xlsx", "s3://bucket/model.zarr")
11
+ wb["Inputs!B2:D10"]
12
+ df = xz.read_excel("s3://bucket/model.zarr", sheet_name="Inputs")
13
+ xz.zarr_to_xlsx("s3://bucket/model.zarr", "roundtrip.xlsx")
14
+ """
15
+
16
+ from . import _version
17
+ from .api import open_workbook, read_excel, xlsx_to_zarr, zarr_to_xlsx
18
+ from .exceptions import ExcelizarrError, FormatVersionError, InvalidReferenceError
19
+ from .kinds import CellKind
20
+ from .workbook import ZarrSheet, ZarrWorkbook
21
+
22
+ __all__ = [
23
+ "CellKind",
24
+ "ExcelizarrError",
25
+ "FormatVersionError",
26
+ "InvalidReferenceError",
27
+ "ZarrSheet",
28
+ "ZarrWorkbook",
29
+ "open_workbook",
30
+ "read_excel",
31
+ "xlsx_to_zarr",
32
+ "zarr_to_xlsx",
33
+ ]
34
+
35
+ __version__ = _version.get_versions()["version"]
excelizarr/_pandas.py ADDED
@@ -0,0 +1,185 @@
1
+ """``pandas.read_excel``, but the "Excel file" is a zarr store.
2
+
3
+ Rather than re-implementing ``read_excel``'s argument handling (``header``,
4
+ ``index_col``, ``usecols``, ``skiprows``, ``nrows``, ``parse_dates``, ``dtype``, ...),
5
+ this plugs a zarr-backed sheet source into pandas' own ``BaseExcelReader``. pandas then
6
+ does everything it does for a real ``.xlsx`` after the cells have been read, so the
7
+ result matches ``pd.read_excel(path, engine="openpyxl")`` by construction, and the
8
+ test suite checks that it does.
9
+
10
+ The one thing pandas asks of a reader is ``get_sheet_data``: rows of Python values,
11
+ converted the way its openpyxl reader converts them (empty → ``""``, error → ``NaN``,
12
+ integral numbers → ``int``), with trailing empty cells and rows trimmed. When pandas
13
+ only needs the first ``n`` rows (``nrows``), only those rows are read from the store.
14
+
15
+ ``BaseExcelReader`` is pandas-internal API. It has been stable for years, but if a
16
+ pandas release moves it, this module is the only place that needs to change.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ import numpy as np
24
+ from pandas.io.excel._base import BaseExcelReader # type: ignore[attr-defined]
25
+
26
+ from .kinds import CellKind, Slot
27
+ from .model import as_ints, decode, value_slots
28
+ from .workbook import ZarrSheet, ZarrWorkbook
29
+
30
+ __all__ = ["ZarrExcelReader", "sheet_data"]
31
+
32
+
33
+ def sheet_data(
34
+ sheet: ZarrSheet, file_rows_needed: int | None = None
35
+ ) -> list[list[Any]]:
36
+ """Read a sheet as pandas' openpyxl reader would hand it to its parser.
37
+
38
+ Parameters
39
+ ----------
40
+ sheet : ZarrSheet
41
+ The sheet.
42
+ file_rows_needed : int, optional
43
+ Read at most this many rows from the top.
44
+
45
+ Returns
46
+ -------
47
+ list of list
48
+ Row-major cell values, rectangular, trailing empties trimmed.
49
+ """
50
+ nrows, ncols = sheet.shape
51
+ if file_rows_needed is not None:
52
+ nrows = min(nrows, file_rows_needed)
53
+ if nrows == 0 or ncols == 0:
54
+ return []
55
+ slots = sheet.slots((slice(0, nrows), slice(0, ncols)))
56
+ kind = slots[Slot.KIND]
57
+ slots.prefetch(value_slots(kind))
58
+ vals = decode(slots)
59
+
60
+ # pandas' openpyxl reader: whole-number floats come back as ints, errors as NaN
61
+ is_float = kind == CellKind.FLOAT
62
+ if is_float.any():
63
+ number = slots[Slot.NUMBER]
64
+ whole = is_float & (number == np.floor(number))
65
+ if whole.any():
66
+ vals[whole] = as_ints(number[whole])
67
+ vals[kind == CellKind.ERROR] = np.nan
68
+
69
+ # pandas' openpyxl reader spells an empty cell "", and trims on that
70
+ empty = kind == CellKind.EMPTY
71
+ vals[empty] = ""
72
+
73
+ filled = ~empty
74
+ if not filled.any():
75
+ return []
76
+ last_row = int(np.flatnonzero(filled.any(axis=1))[-1]) + 1
77
+ last_col = int(np.flatnonzero(filled.any(axis=0))[-1]) + 1
78
+ rows: list[list[Any]] = vals[:last_row, :last_col].tolist()
79
+ return rows
80
+
81
+
82
+ class ZarrExcelReader(BaseExcelReader[ZarrWorkbook]):
83
+ """A pandas Excel reader whose "file" is a :class:`~.workbook.ZarrWorkbook`.
84
+
85
+ Parameters
86
+ ----------
87
+ workbook : ZarrWorkbook
88
+ The workbook to read sheets from.
89
+ """
90
+
91
+ def __init__(self, workbook: ZarrWorkbook) -> None:
92
+ super().__init__(workbook)
93
+
94
+ @property
95
+ def _workbook_class(self) -> type[ZarrWorkbook]:
96
+ return ZarrWorkbook
97
+
98
+ def load_workbook(
99
+ self, filepath_or_buffer: Any, engine_kwargs: Any
100
+ ) -> ZarrWorkbook:
101
+ """Never called: the workbook is always handed over already open.
102
+
103
+ Parameters
104
+ ----------
105
+ filepath_or_buffer : object
106
+ Unused.
107
+ engine_kwargs : object
108
+ Unused.
109
+
110
+ Returns
111
+ -------
112
+ ZarrWorkbook
113
+ Never returns.
114
+
115
+ Raises
116
+ ------
117
+ TypeError
118
+ Always.
119
+ """
120
+ raise TypeError("ZarrExcelReader takes an open ZarrWorkbook")
121
+
122
+ @property
123
+ def sheet_names(self) -> list[str]:
124
+ """Return the workbook's sheet names, in tab order.
125
+
126
+ Returns
127
+ -------
128
+ list of str
129
+ Sheet names, in tab order.
130
+ """
131
+ book: ZarrWorkbook = self.book
132
+ return book.sheet_names
133
+
134
+ def get_sheet_by_name(self, name: str) -> ZarrSheet:
135
+ """Return a sheet by name, with pandas' error for a missing one.
136
+
137
+ Parameters
138
+ ----------
139
+ name : str
140
+ The sheet name.
141
+
142
+ Returns
143
+ -------
144
+ ZarrSheet
145
+ The sheet.
146
+ """
147
+ self.raise_if_bad_sheet_by_name(name)
148
+ book: ZarrWorkbook = self.book
149
+ return book.sheet(name)
150
+
151
+ def get_sheet_by_index(self, index: int) -> ZarrSheet:
152
+ """Return a sheet by tab position, with pandas' error for a missing one.
153
+
154
+ Parameters
155
+ ----------
156
+ index : int
157
+ Zero-based tab position.
158
+
159
+ Returns
160
+ -------
161
+ ZarrSheet
162
+ The sheet.
163
+ """
164
+ self.raise_if_bad_sheet_by_index(index)
165
+ book: ZarrWorkbook = self.book
166
+ return book.sheet(index)
167
+
168
+ def get_sheet_data(
169
+ self, sheet: ZarrSheet, file_rows_needed: int | None = None
170
+ ) -> list[list[Any]]:
171
+ """Rows of Python values for pandas' parser; see :func:`sheet_data`.
172
+
173
+ Parameters
174
+ ----------
175
+ sheet : ZarrSheet
176
+ The sheet.
177
+ file_rows_needed : int, optional
178
+ Read at most this many rows from the top.
179
+
180
+ Returns
181
+ -------
182
+ list of list
183
+ Row-major cell values.
184
+ """
185
+ return sheet_data(sheet, file_rows_needed)
excelizarr/_refs.py ADDED
@@ -0,0 +1,226 @@
1
+ """A1-style references: ``"B2"``, ``"A:C"``, ``"'My Sheet'!$B$2:D10"``.
2
+
3
+ Everything here works in **zero-based, half-open** coordinates, which is what the zarr
4
+ arrays are indexed in: ``"B2:D10"`` is rows ``1:10`` and columns ``1:4``. Whole-column
5
+ and whole-row ranges (``"A:C"``, ``"2:5"``) leave the other axis open (``None``), and
6
+ it is the caller's job to clip that to the sheet's shape.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+
14
+ from .exceptions import InvalidReferenceError
15
+
16
+ __all__ = [
17
+ "MAX_COLS",
18
+ "MAX_ROWS",
19
+ "RangeRef",
20
+ "col_to_index",
21
+ "format_range",
22
+ "index_to_col",
23
+ "parse_range",
24
+ ]
25
+
26
+ #: Excel's own limits: ``XFD1048576`` is the last cell of a sheet.
27
+ MAX_ROWS = 1_048_576
28
+ MAX_COLS = 16_384
29
+
30
+ _CELL = re.compile(r"^\$?([A-Za-z]{1,3})\$?([0-9]+)$")
31
+ _COL = re.compile(r"^\$?([A-Za-z]{1,3})$")
32
+ _ROW = re.compile(r"^\$?([0-9]+)$")
33
+
34
+
35
+ def col_to_index(letters: str) -> int:
36
+ """Convert column letters to a zero-based index (``"A"`` is 0, ``"AA"`` is 26).
37
+
38
+ Parameters
39
+ ----------
40
+ letters : str
41
+ One to three letters, either case.
42
+
43
+ Returns
44
+ -------
45
+ int
46
+ The zero-based column index.
47
+
48
+ Raises
49
+ ------
50
+ InvalidReferenceError
51
+ If ``letters`` isn't a column Excel could have.
52
+ """
53
+ if (
54
+ not letters
55
+ or not letters.isascii()
56
+ or not letters.isalpha()
57
+ or len(letters) > 3
58
+ ):
59
+ raise InvalidReferenceError(f"not a column: {letters!r}")
60
+ n = 0
61
+ for ch in letters.upper():
62
+ n = n * 26 + (ord(ch) - ord("A") + 1)
63
+ if n > MAX_COLS:
64
+ raise InvalidReferenceError(f"column {letters!r} is past XFD")
65
+ return n - 1
66
+
67
+
68
+ def index_to_col(index: int) -> str:
69
+ """Convert a zero-based column index to letters (0 is ``"A"``, 26 is ``"AA"``).
70
+
71
+ Parameters
72
+ ----------
73
+ index : int
74
+ A column index in ``[0, MAX_COLS)``.
75
+
76
+ Returns
77
+ -------
78
+ str
79
+ The upper-case column letters.
80
+
81
+ Raises
82
+ ------
83
+ InvalidReferenceError
84
+ If ``index`` is outside the sheet.
85
+ """
86
+ if not 0 <= index < MAX_COLS:
87
+ raise InvalidReferenceError(f"column index {index} is outside the sheet")
88
+ out = ""
89
+ n = index + 1
90
+ while n:
91
+ n, rem = divmod(n - 1, 26)
92
+ out = chr(ord("A") + rem) + out
93
+ return out
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class RangeRef:
98
+ """A parsed range: an optional sheet plus half-open, zero-based row/col bounds.
99
+
100
+ ``None`` on a bound means "open on that side": ``"A:C"`` has ``row_start`` and
101
+ ``row_stop`` both ``None``.
102
+ """
103
+
104
+ sheet: str | None
105
+ row_start: int | None
106
+ row_stop: int | None
107
+ col_start: int | None
108
+ col_stop: int | None
109
+
110
+ def resolve(self, shape: tuple[int, int]) -> tuple[slice, slice]:
111
+ """Clip the open bounds to a sheet ``shape`` and return two slices.
112
+
113
+ Parameters
114
+ ----------
115
+ shape : tuple of int
116
+ The ``(rows, cols)`` extent of the sheet.
117
+
118
+ Returns
119
+ -------
120
+ tuple of slice
121
+ ``(rows, cols)`` slices, never reaching past ``shape``.
122
+ """
123
+ nrows, ncols = shape
124
+ r0 = 0 if self.row_start is None else min(self.row_start, nrows)
125
+ r1 = nrows if self.row_stop is None else min(self.row_stop, nrows)
126
+ c0 = 0 if self.col_start is None else min(self.col_start, ncols)
127
+ c1 = ncols if self.col_stop is None else min(self.col_stop, ncols)
128
+ return slice(r0, max(r0, r1)), slice(c0, max(c0, c1))
129
+
130
+
131
+ def _split_sheet(ref: str) -> tuple[str | None, str]:
132
+ if "!" not in ref:
133
+ return None, ref
134
+ sheet, _, rest = ref.rpartition("!")
135
+ if len(sheet) >= 2 and sheet[0] == sheet[-1] == "'":
136
+ sheet = sheet[1:-1].replace("''", "'")
137
+ if not sheet:
138
+ raise InvalidReferenceError(f"empty sheet name in {ref!r}")
139
+ return sheet, rest
140
+
141
+
142
+ def _parse_endpoint(part: str) -> tuple[int | None, int | None]:
143
+ """Parse one side of a range into (row, col), either of which may be open."""
144
+ if m := _CELL.match(part):
145
+ row = int(m.group(2))
146
+ if not 1 <= row <= MAX_ROWS:
147
+ raise InvalidReferenceError(f"row {row} is outside the sheet")
148
+ return row - 1, col_to_index(m.group(1))
149
+ if m := _COL.match(part):
150
+ return None, col_to_index(m.group(1))
151
+ if m := _ROW.match(part):
152
+ row = int(m.group(1))
153
+ if not 1 <= row <= MAX_ROWS:
154
+ raise InvalidReferenceError(f"row {row} is outside the sheet")
155
+ return row - 1, None
156
+ raise InvalidReferenceError(f"not a cell, column or row: {part!r}")
157
+
158
+
159
+ def parse_range(ref: str) -> RangeRef:
160
+ """Parse an A1-style reference into a :class:`RangeRef`.
161
+
162
+ Accepts a single cell (``"B2"``), a rectangle (``"B2:D10"``, either corner first),
163
+ whole columns (``"A:C"``), whole rows (``"2:5"``), ``$`` absolute markers and an
164
+ optional sheet prefix, quoted the way Excel quotes it (``"'Q1 ''24'!A1"``).
165
+
166
+ Parameters
167
+ ----------
168
+ ref : str
169
+ The reference to parse.
170
+
171
+ Returns
172
+ -------
173
+ RangeRef
174
+ Zero-based, half-open bounds.
175
+
176
+ Raises
177
+ ------
178
+ InvalidReferenceError
179
+ If ``ref`` doesn't parse, or mixes a column-only end with a row-only end.
180
+ """
181
+ sheet, body = _split_sheet(ref.strip())
182
+ parts = body.split(":")
183
+ if len(parts) == 1:
184
+ if not _CELL.match(parts[0]):
185
+ raise InvalidReferenceError(f"not a cell reference: {ref!r}")
186
+ parts = [parts[0], parts[0]]
187
+ if len(parts) != 2:
188
+ raise InvalidReferenceError(f"too many ':' in {ref!r}")
189
+ (ra, ca), (rb, cb) = (_parse_endpoint(p) for p in parts)
190
+ if (ra is None) != (rb is None) or (ca is None) != (cb is None):
191
+ raise InvalidReferenceError(f"mismatched range ends in {ref!r}")
192
+ if ra is None and ca is None: # pragma: no cover - unreachable by the regexes
193
+ raise InvalidReferenceError(f"empty range {ref!r}")
194
+
195
+ def bounds(a: int | None, b: int | None) -> tuple[int | None, int | None]:
196
+ if a is None or b is None:
197
+ return None, None
198
+ return min(a, b), max(a, b) + 1
199
+
200
+ r0, r1 = bounds(ra, rb)
201
+ c0, c1 = bounds(ca, cb)
202
+ return RangeRef(sheet, r0, r1, c0, c1)
203
+
204
+
205
+ def format_range(row_start: int, row_stop: int, col_start: int, col_stop: int) -> str:
206
+ """Format half-open zero-based bounds back to an A1 range (``"B2:D10"``).
207
+
208
+ Parameters
209
+ ----------
210
+ row_start : int
211
+ First row, zero-based.
212
+ row_stop : int
213
+ One past the last row.
214
+ col_start : int
215
+ First column, zero-based.
216
+ col_stop : int
217
+ One past the last column.
218
+
219
+ Returns
220
+ -------
221
+ str
222
+ The A1 range, or a single cell reference when the range is one cell.
223
+ """
224
+ top_left = f"{index_to_col(col_start)}{row_start + 1}"
225
+ bottom_right = f"{index_to_col(col_stop - 1)}{row_stop}"
226
+ return top_left if top_left == bottom_right else f"{top_left}:{bottom_right}"
excelizarr/_version.py ADDED
@@ -0,0 +1,21 @@
1
+
2
+ # This file was generated by 'versioneer.py' (0.29) from
3
+ # revision-control system data, or from the parent directory name of an
4
+ # unpacked source archive. Distribution tarballs contain a pre-generated copy
5
+ # of this file.
6
+
7
+ import json
8
+
9
+ version_json = '''
10
+ {
11
+ "date": "2026-10-12T17:45:19+1100",
12
+ "dirty": false,
13
+ "error": null,
14
+ "full-revisionid": "803bf2468fccbd6a42dd13476df85b208c3a2899",
15
+ "version": "0.1.0"
16
+ }
17
+ ''' # END VERSION_JSON
18
+
19
+
20
+ def get_versions():
21
+ return json.loads(version_json)
excelizarr/api.py ADDED
@@ -0,0 +1,155 @@
1
+ """The top-level verbs: convert, open, read, convert back."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ import zarr
9
+ from zarr.storage import StoreLike
10
+
11
+ from .io_xlsx import Engine, load_xlsx
12
+ from .io_zarr import DEFAULT_CHUNKS, DEFAULT_SHARDS, open_root, write_model
13
+ from .workbook import ZarrWorkbook
14
+
15
+ __all__ = ["open_workbook", "read_excel", "xlsx_to_zarr", "zarr_to_xlsx"]
16
+
17
+
18
+ def xlsx_to_zarr(
19
+ src: str | os.PathLike[str],
20
+ store: StoreLike,
21
+ *,
22
+ engine: Engine = "openpyxl",
23
+ chunks: tuple[int, int] = DEFAULT_CHUNKS,
24
+ shards: tuple[int, int] | None = DEFAULT_SHARDS,
25
+ mode: str = "w-",
26
+ storage_options: dict[str, Any] | None = None,
27
+ ) -> ZarrWorkbook:
28
+ """Convert an ``.xlsx`` file into a zarr workbook store.
29
+
30
+ Parameters
31
+ ----------
32
+ src : str or os.PathLike
33
+ The ``.xlsx`` file.
34
+ store : StoreLike
35
+ Where to write: a path, an ``s3://``/``gs://`` URL, a ``MemoryStore``, a
36
+ ``ZipStore``, an Icechunk session's ``store``, ...
37
+ engine : {"openpyxl", "calamine"}
38
+ ``openpyxl`` keeps formulas, number formats and layout; ``calamine`` (the
39
+ ``fast`` extra) keeps values only, and is much faster.
40
+ chunks : tuple of int
41
+ ``(rows, cols)`` per chunk.
42
+ shards : tuple of int or None
43
+ ``(rows, cols)`` per shard, or ``None`` to write every chunk as its own
44
+ object.
45
+ mode : str
46
+ ``"w-"`` (default) refuses to overwrite an existing store; ``"w"`` replaces it.
47
+ storage_options : dict, optional
48
+ Passed through to fsspec for URL stores.
49
+
50
+ Returns
51
+ -------
52
+ ZarrWorkbook
53
+ The written workbook, open for reading.
54
+ """
55
+ model = load_xlsx(src, engine=engine)
56
+ root = write_model(
57
+ model,
58
+ store,
59
+ chunks=chunks,
60
+ shards=shards,
61
+ mode=mode,
62
+ storage_options=storage_options,
63
+ )
64
+ return ZarrWorkbook(root)
65
+
66
+
67
+ def open_workbook(
68
+ store: StoreLike | zarr.Group,
69
+ *,
70
+ mode: str = "r",
71
+ storage_options: dict[str, Any] | None = None,
72
+ ) -> ZarrWorkbook:
73
+ """Open a zarr workbook store lazily.
74
+
75
+ Parameters
76
+ ----------
77
+ store : StoreLike or zarr.Group
78
+ The store, or its already-open root group.
79
+ mode : str
80
+ ``"r"`` (default) for read-only; ``"r+"`` to allow in-place cell writes.
81
+ storage_options : dict, optional
82
+ Passed through to fsspec for URL stores.
83
+
84
+ Returns
85
+ -------
86
+ ZarrWorkbook
87
+ The workbook. Nothing but its root attributes has been read yet.
88
+ """
89
+ return ZarrWorkbook(open_root(store, mode=mode, storage_options=storage_options))
90
+
91
+
92
+ def read_excel(
93
+ store: StoreLike | zarr.Group | ZarrWorkbook,
94
+ sheet_name: str | int | list[str | int] | None = 0,
95
+ *,
96
+ storage_options: dict[str, Any] | None = None,
97
+ **kwargs: Any,
98
+ ) -> Any:
99
+ """``pandas.read_excel``, reading from a zarr workbook store instead of ``.xlsx``.
100
+
101
+ pandas' own argument handling runs on the cells, so every keyword ``read_excel``
102
+ takes works the same way here, and the frame you get is the frame you'd have got
103
+ from the original file.
104
+
105
+ Parameters
106
+ ----------
107
+ store : StoreLike, zarr.Group or ZarrWorkbook
108
+ The workbook.
109
+ sheet_name : str, int, list or None
110
+ As for ``pandas.read_excel``: a name, a position, a list of either (returns a
111
+ dict), or ``None`` for every sheet (returns a dict).
112
+ storage_options : dict, optional
113
+ Passed through to fsspec for URL stores.
114
+ **kwargs
115
+ Anything else ``pandas.read_excel`` takes: ``header``, ``index_col``,
116
+ ``usecols``, ``skiprows``, ``nrows``, ``dtype``, ``parse_dates``, ...
117
+
118
+ Returns
119
+ -------
120
+ pandas.DataFrame or dict of pandas.DataFrame
121
+ As ``pandas.read_excel`` would return.
122
+ """
123
+ from ._pandas import ZarrExcelReader
124
+
125
+ wb = (
126
+ store
127
+ if isinstance(store, ZarrWorkbook)
128
+ else open_workbook(store, storage_options=storage_options)
129
+ )
130
+ return ZarrExcelReader(wb).parse(sheet_name=sheet_name, **kwargs)
131
+
132
+
133
+ def zarr_to_xlsx(
134
+ store: StoreLike | zarr.Group | ZarrWorkbook,
135
+ dst: str | os.PathLike[str],
136
+ *,
137
+ storage_options: dict[str, Any] | None = None,
138
+ ) -> None:
139
+ """Convert a zarr workbook store back into an ``.xlsx`` file.
140
+
141
+ Parameters
142
+ ----------
143
+ store : StoreLike, zarr.Group or ZarrWorkbook
144
+ The workbook.
145
+ dst : str or os.PathLike
146
+ The ``.xlsx`` file to write.
147
+ storage_options : dict, optional
148
+ Passed through to fsspec for URL stores.
149
+ """
150
+ wb = (
151
+ store
152
+ if isinstance(store, ZarrWorkbook)
153
+ else open_workbook(store, storage_options=storage_options)
154
+ )
155
+ wb.to_xlsx(dst)
@@ -0,0 +1,15 @@
1
+ """The exceptions excelizarr raises on purpose."""
2
+
3
+ __all__ = ["ExcelizarrError", "FormatVersionError", "InvalidReferenceError"]
4
+
5
+
6
+ class ExcelizarrError(Exception):
7
+ """Base class for every error excelizarr raises deliberately."""
8
+
9
+
10
+ class InvalidReferenceError(ExcelizarrError, ValueError):
11
+ """An A1-style cell or range reference that doesn't parse, or names no sheet."""
12
+
13
+
14
+ class FormatVersionError(ExcelizarrError):
15
+ """A store that isn't an excelizarr workbook, or is one from a format we can't read."""