pyhandlexl 0.2.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lewis Wainaina
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyhandlexl
3
+ Version: 0.2.0
4
+ Summary: Use an Excel file as the database for your next project.
5
+ Author-email: Lewis Wainaina <lewyamendi@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/LewyAmendi/pyhandlexl
8
+ Project-URL: Repository, https://github.com/LewyAmendi/pyhandlexl
9
+ Project-URL: Issues, https://github.com/LewyAmendi/pyhandlexl/issues
10
+ Project-URL: Changelog, https://github.com/LewyAmendi/pyhandlexl/blob/main/CHANGELOG.md
11
+ Keywords: excel,xlsx,openpyxl,spreadsheet,xl
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: openpyxl>=3.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: ruff>=0.6; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # pyhandlexl
33
+
34
+ [![CI](https://github.com/LewyAmendi/pyhandlexl/actions/workflows/ci.yml/badge.svg)](https://github.com/LewyAmendi/pyhandlexl/actions/workflows/ci.yml)
35
+
36
+ **Make an Excel file the database for your next project.**
37
+
38
+ A spreadsheet is the most portable data store there is: every machine opens it,
39
+ anyone can read or edit it without knowing a query language, it versions as a
40
+ single file, and there is no server to run. `pyhandlexl` makes driving one from
41
+ Python dependable — read and write raw cell values in a known order, with writes
42
+ that leave the file intact even when things go wrong.
43
+
44
+ Built on [openpyxl](https://openpyxl.readthedocs.io/) and built to grow.
45
+
46
+ - **`Table`** — the main way in. Row 1 holds your column headers, column A holds
47
+ your row labels, and everything from `B2` on is data. Read it, edit it by name,
48
+ write it back.
49
+ - **`read_sheet` / `write_sheet`** — direct grid access for sheets that aren't a
50
+ labelled table.
51
+
52
+ > **Version 0.1.0.** Usable today and under active development — expect new
53
+ > capabilities with each release, and some API changes as it matures.
54
+
55
+ ## Install
56
+
57
+ Not on PyPI yet. From source:
58
+
59
+ ```bash
60
+ git clone https://github.com/LewyAmendi/pyhandlexl
61
+ cd pyhandlexl
62
+ pip install -e .
63
+ ```
64
+
65
+ Requires Python 3.10+.
66
+
67
+ ## Quickstart
68
+
69
+ Given `budget.xlsx`:
70
+
71
+ | | q1 | q2 |
72
+ |--------|----|----|
73
+ | **Alice** | 10 | 20 |
74
+ | **Bob** | 30 | 40 |
75
+
76
+ ```python
77
+ from pyhandlexl import Table
78
+
79
+ t = Table.read("budget.xlsx")
80
+
81
+ t.read_cell(row="Alice", column="q2") # '20'
82
+ t.read_row("Bob") # ['30', '40']
83
+ t.read_column("q1") # ['10', '30']
84
+
85
+ t.set_cell(row="Alice", column="q1", value=99) # edit in place
86
+ t.add_row("Carol", [1, 2])
87
+ t.write("budget.xlsx") # one safe, atomic write
88
+ ```
89
+
90
+ > **Values are always strings.** `read_sheet` and `Table` coerce every cell to
91
+ > `str` (empty cells become `""`). Convert to numbers yourself where you need to.
92
+
93
+ ## The `Table` class
94
+
95
+ ### Reading
96
+
97
+ ```python
98
+ Table.read(path, sheet=None, *, column_headers=True, row_labels=True)
99
+ ```
100
+
101
+ - `sheet=None` reads the active sheet; pass a name for a specific one.
102
+ - `column_headers=False` — row 1 is ordinary data, `column_headers` is empty.
103
+ - `row_labels=False` — column A is ordinary data, `row_labels` is empty.
104
+
105
+ Row labels and column headers are always `str` — required if you build a
106
+ `Table` by hand, too (`Table(..., column_headers=[1, 2])` raises `TypeError`).
107
+
108
+ ### The whole table at once
109
+
110
+ ```python
111
+ t.corner # value of cell A1 (settable: t.corner = "name")
112
+ t.data # a TableData snapshot
113
+ ```
114
+
115
+ ```python
116
+ d = t.data
117
+ d.rows # [['10', '20'], ['30', '40']] (B2 onward, by row)
118
+ d.columns # [['10', '30'], ['20', '40']] (same data, by column)
119
+ d.row_labels # ['Alice', 'Bob'] (column A, from A2)
120
+ d.column_headers # ['q1', 'q2'] (row 1, from B1)
121
+ d.corner # value of cell A1
122
+ ```
123
+
124
+ Every field is a fresh copy — mutating `t.data.rows` does not change the table.
125
+
126
+ ### Access by label
127
+
128
+ ```python
129
+ t.read_row("Bob") # a data row (no label)
130
+ t.read_column("q1") # a data column (no header)
131
+ ```
132
+
133
+ Unknown labels raise `KeyError`. If a label appears twice, the first match wins.
134
+
135
+ ### `read_cell` / `set_cell` — a single value, by position or by label
136
+
137
+ Both take the same addressing: a ref like `"B2"`, or `row=`/`column=` as a
138
+ matching pair — **both ints** for a 1-based Excel position (row 1 is the
139
+ header row, column 1 is the label column), or **both strings** for a row
140
+ label / column header pair. Mixing types raises `TypeError`.
141
+
142
+ ```python
143
+ t.read_cell("B2") # '10' — first data cell, by position
144
+ t.read_cell(row=2, column=2) # '10' — same thing, spelled out
145
+ t.read_cell(row="Alice", column="q1") # '10' — same value, by label
146
+
147
+ t.read_cell(row=1, column=2) # 'q1' — a column header
148
+ t.read_cell(row=2, column=1) # 'Alice' — a row label
149
+ t.read_cell("A1") # the corner
150
+ ```
151
+
152
+ By position, `read_cell` can reach *any* cell — header, label, corner, or
153
+ data. By label it always reads data, wherever that row/column intersection
154
+ actually lives.
155
+
156
+ A string given through `row=`/`column=` is always a label lookup — it does
157
+ **not** accept a column letter like `"B"` for a position. Use a plain number
158
+ (`column=2`) or `ref="B2"` for letter-based positions.
159
+
160
+ ### Editing (in place, returns `None`)
161
+
162
+ ```python
163
+ t.set_cell("B2", value=99) # by position — ref
164
+ t.set_cell(row=2, column=2, value=99) # by position — row=/column= as ints
165
+ t.set_cell(row="Alice", column="q1", value=99) # by label — row=/column= as strings
166
+ t.set_row("Bob", [50, 60]) # replace a row (length must match)
167
+ t.set_column("q1", [1, 2]) # replace a column (length must match)
168
+
169
+ t.add_row("Carol", [1, 2]) # append a labelled row
170
+ t.add_column("q3", [5, 6]) # append a labelled column
171
+
172
+ t.drop_row("Bob")
173
+ t.drop_column("q2")
174
+
175
+ t.rename_row("Alice", "ALICE")
176
+ t.rename_column("q1", "Q1")
177
+ t.corner = "name"
178
+ ```
179
+
180
+ `set_cell` only ever touches **data** — addressing a header, row label, or the
181
+ corner by position raises `ValueError`; use `rename_row`, `rename_column`, or
182
+ `t.corner = value` for those. Wrong-length values raise `ValueError`; unknown
183
+ labels raise `KeyError`; a non-`str` row label or column header (in `add_row`,
184
+ `add_column`, `rename_row`, `rename_column`) raises `TypeError`.
185
+
186
+ You can also build a table from nothing:
187
+
188
+ ```python
189
+ t = Table([], column_headers=["q1", "q2"])
190
+ t.add_row("Alice", [10, 20])
191
+ t.write("new.xlsx")
192
+ ```
193
+
194
+ ### Writing
195
+
196
+ ```python
197
+ t.write(path, sheet=None)
198
+ ```
199
+
200
+ Reassembles headers into row 1 and labels into column A, then writes the whole
201
+ sheet. Other sheets in the file are left untouched.
202
+
203
+ ### Equality
204
+
205
+ ```python
206
+ t1 == t2 # compares data, headers, labels, corner
207
+ ```
208
+
209
+ Row count and row-label membership go through `t.data` instead of `len()`/`in`,
210
+ so the call site says what's being checked: `len(t.data.rows)`,
211
+ `"Bob" in t.data.row_labels`.
212
+
213
+ ## The raw layer
214
+
215
+ For sheets that are not a labelled table — plain grids, exports, odd layouts.
216
+
217
+ ```python
218
+ from pyhandlexl import read_sheet, write_sheet, append_rows
219
+
220
+ read_sheet(path, sheet=None, *, pad=False)
221
+ ```
222
+
223
+ Returns `list[list[str]]`. Trailing empty cells are trimmed from each row (a
224
+ fully empty row becomes `[]`); `pad=True` right-pads every row to the widest
225
+ row's length instead.
226
+
227
+ ```python
228
+ write_sheet(path, rows, sheet=None, *, orientation="rows")
229
+ ```
230
+
231
+ Replaces the target sheet with `rows` (other sheets untouched), creating the
232
+ file and sheet if needed. Values are written as-is — `str` stays `str`, `int`
233
+ stays `int`, `None` leaves the cell empty; there is no string-to-number
234
+ conversion. `orientation="columns"` writes each inner list *down a column*
235
+ instead of across a row.
236
+
237
+ ```python
238
+ append_rows(path, rows, sheet=None)
239
+ ```
240
+
241
+ Appends after the last row. Empty input is a no-op.
242
+
243
+ ## Sheet management
244
+
245
+ ```python
246
+ from pyhandlexl import (
247
+ list_sheets, sheet_exists, create_sheet, delete_sheet, rename_sheet,
248
+ )
249
+
250
+ list_sheets(path) # ['Sheet1', 'Data']
251
+ sheet_exists(path, "Data") # True
252
+ create_sheet(path, "Results") # ValueError if it already exists
253
+ delete_sheet(path, "Old") # refuses to delete the last sheet
254
+ rename_sheet(path, "Old", "New")
255
+ ```
256
+
257
+ Sheet names are validated everywhere: max 31 characters, none of `\ / ? * [ ] :`,
258
+ and `"History"` is reserved by Excel.
259
+
260
+ ## Safe writes
261
+
262
+ Every write goes through the same steps:
263
+
264
+ 1. Save to a temporary file in the same directory.
265
+ 2. Verify it is a readable `.xlsx`.
266
+ 3. Atomically replace the original (`os.replace`).
267
+
268
+ If any step fails the temporary file is removed and the original is left exactly
269
+ as it was. If the target is locked (open in Excel), writes retry briefly before
270
+ raising `FileLockedError`.
271
+
272
+ ## Errors
273
+
274
+ All raised exceptions derive from `PyhandlexlError`:
275
+
276
+ | Exception | Also a | Meaning |
277
+ |---|---|---|
278
+ | `SheetNameError` | `ValueError` | invalid worksheet name |
279
+ | `DimensionError` | `ValueError` | data exceeds Excel's 1,048,576 × 16,384 grid |
280
+ | `SheetNotFoundError` | `KeyError` | no worksheet with that name |
281
+ | `FileLockedError` | `OSError` | file stayed locked through every retry |
282
+ | `InvalidFileError` | — | file is missing or not a readable `.xlsx` |
283
+
284
+ ## Not in scope
285
+
286
+ `pyhandlexl` deliberately does **not** handle: cell formatting, styles, fonts,
287
+ formulas, charts, images, merged cells, `.xls` (old format), or password
288
+ protection / encryption. For any of that, use openpyxl directly.
289
+
290
+ ## Development
291
+
292
+ ```bash
293
+ python -m venv .venv
294
+ source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
295
+ pip install -e ".[dev]"
296
+ pytest
297
+ ruff check . && ruff format --check .
298
+ ```
299
+
300
+ ## License
301
+
302
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,271 @@
1
+ # pyhandlexl
2
+
3
+ [![CI](https://github.com/LewyAmendi/pyhandlexl/actions/workflows/ci.yml/badge.svg)](https://github.com/LewyAmendi/pyhandlexl/actions/workflows/ci.yml)
4
+
5
+ **Make an Excel file the database for your next project.**
6
+
7
+ A spreadsheet is the most portable data store there is: every machine opens it,
8
+ anyone can read or edit it without knowing a query language, it versions as a
9
+ single file, and there is no server to run. `pyhandlexl` makes driving one from
10
+ Python dependable — read and write raw cell values in a known order, with writes
11
+ that leave the file intact even when things go wrong.
12
+
13
+ Built on [openpyxl](https://openpyxl.readthedocs.io/) and built to grow.
14
+
15
+ - **`Table`** — the main way in. Row 1 holds your column headers, column A holds
16
+ your row labels, and everything from `B2` on is data. Read it, edit it by name,
17
+ write it back.
18
+ - **`read_sheet` / `write_sheet`** — direct grid access for sheets that aren't a
19
+ labelled table.
20
+
21
+ > **Version 0.1.0.** Usable today and under active development — expect new
22
+ > capabilities with each release, and some API changes as it matures.
23
+
24
+ ## Install
25
+
26
+ Not on PyPI yet. From source:
27
+
28
+ ```bash
29
+ git clone https://github.com/LewyAmendi/pyhandlexl
30
+ cd pyhandlexl
31
+ pip install -e .
32
+ ```
33
+
34
+ Requires Python 3.10+.
35
+
36
+ ## Quickstart
37
+
38
+ Given `budget.xlsx`:
39
+
40
+ | | q1 | q2 |
41
+ |--------|----|----|
42
+ | **Alice** | 10 | 20 |
43
+ | **Bob** | 30 | 40 |
44
+
45
+ ```python
46
+ from pyhandlexl import Table
47
+
48
+ t = Table.read("budget.xlsx")
49
+
50
+ t.read_cell(row="Alice", column="q2") # '20'
51
+ t.read_row("Bob") # ['30', '40']
52
+ t.read_column("q1") # ['10', '30']
53
+
54
+ t.set_cell(row="Alice", column="q1", value=99) # edit in place
55
+ t.add_row("Carol", [1, 2])
56
+ t.write("budget.xlsx") # one safe, atomic write
57
+ ```
58
+
59
+ > **Values are always strings.** `read_sheet` and `Table` coerce every cell to
60
+ > `str` (empty cells become `""`). Convert to numbers yourself where you need to.
61
+
62
+ ## The `Table` class
63
+
64
+ ### Reading
65
+
66
+ ```python
67
+ Table.read(path, sheet=None, *, column_headers=True, row_labels=True)
68
+ ```
69
+
70
+ - `sheet=None` reads the active sheet; pass a name for a specific one.
71
+ - `column_headers=False` — row 1 is ordinary data, `column_headers` is empty.
72
+ - `row_labels=False` — column A is ordinary data, `row_labels` is empty.
73
+
74
+ Row labels and column headers are always `str` — required if you build a
75
+ `Table` by hand, too (`Table(..., column_headers=[1, 2])` raises `TypeError`).
76
+
77
+ ### The whole table at once
78
+
79
+ ```python
80
+ t.corner # value of cell A1 (settable: t.corner = "name")
81
+ t.data # a TableData snapshot
82
+ ```
83
+
84
+ ```python
85
+ d = t.data
86
+ d.rows # [['10', '20'], ['30', '40']] (B2 onward, by row)
87
+ d.columns # [['10', '30'], ['20', '40']] (same data, by column)
88
+ d.row_labels # ['Alice', 'Bob'] (column A, from A2)
89
+ d.column_headers # ['q1', 'q2'] (row 1, from B1)
90
+ d.corner # value of cell A1
91
+ ```
92
+
93
+ Every field is a fresh copy — mutating `t.data.rows` does not change the table.
94
+
95
+ ### Access by label
96
+
97
+ ```python
98
+ t.read_row("Bob") # a data row (no label)
99
+ t.read_column("q1") # a data column (no header)
100
+ ```
101
+
102
+ Unknown labels raise `KeyError`. If a label appears twice, the first match wins.
103
+
104
+ ### `read_cell` / `set_cell` — a single value, by position or by label
105
+
106
+ Both take the same addressing: a ref like `"B2"`, or `row=`/`column=` as a
107
+ matching pair — **both ints** for a 1-based Excel position (row 1 is the
108
+ header row, column 1 is the label column), or **both strings** for a row
109
+ label / column header pair. Mixing types raises `TypeError`.
110
+
111
+ ```python
112
+ t.read_cell("B2") # '10' — first data cell, by position
113
+ t.read_cell(row=2, column=2) # '10' — same thing, spelled out
114
+ t.read_cell(row="Alice", column="q1") # '10' — same value, by label
115
+
116
+ t.read_cell(row=1, column=2) # 'q1' — a column header
117
+ t.read_cell(row=2, column=1) # 'Alice' — a row label
118
+ t.read_cell("A1") # the corner
119
+ ```
120
+
121
+ By position, `read_cell` can reach *any* cell — header, label, corner, or
122
+ data. By label it always reads data, wherever that row/column intersection
123
+ actually lives.
124
+
125
+ A string given through `row=`/`column=` is always a label lookup — it does
126
+ **not** accept a column letter like `"B"` for a position. Use a plain number
127
+ (`column=2`) or `ref="B2"` for letter-based positions.
128
+
129
+ ### Editing (in place, returns `None`)
130
+
131
+ ```python
132
+ t.set_cell("B2", value=99) # by position — ref
133
+ t.set_cell(row=2, column=2, value=99) # by position — row=/column= as ints
134
+ t.set_cell(row="Alice", column="q1", value=99) # by label — row=/column= as strings
135
+ t.set_row("Bob", [50, 60]) # replace a row (length must match)
136
+ t.set_column("q1", [1, 2]) # replace a column (length must match)
137
+
138
+ t.add_row("Carol", [1, 2]) # append a labelled row
139
+ t.add_column("q3", [5, 6]) # append a labelled column
140
+
141
+ t.drop_row("Bob")
142
+ t.drop_column("q2")
143
+
144
+ t.rename_row("Alice", "ALICE")
145
+ t.rename_column("q1", "Q1")
146
+ t.corner = "name"
147
+ ```
148
+
149
+ `set_cell` only ever touches **data** — addressing a header, row label, or the
150
+ corner by position raises `ValueError`; use `rename_row`, `rename_column`, or
151
+ `t.corner = value` for those. Wrong-length values raise `ValueError`; unknown
152
+ labels raise `KeyError`; a non-`str` row label or column header (in `add_row`,
153
+ `add_column`, `rename_row`, `rename_column`) raises `TypeError`.
154
+
155
+ You can also build a table from nothing:
156
+
157
+ ```python
158
+ t = Table([], column_headers=["q1", "q2"])
159
+ t.add_row("Alice", [10, 20])
160
+ t.write("new.xlsx")
161
+ ```
162
+
163
+ ### Writing
164
+
165
+ ```python
166
+ t.write(path, sheet=None)
167
+ ```
168
+
169
+ Reassembles headers into row 1 and labels into column A, then writes the whole
170
+ sheet. Other sheets in the file are left untouched.
171
+
172
+ ### Equality
173
+
174
+ ```python
175
+ t1 == t2 # compares data, headers, labels, corner
176
+ ```
177
+
178
+ Row count and row-label membership go through `t.data` instead of `len()`/`in`,
179
+ so the call site says what's being checked: `len(t.data.rows)`,
180
+ `"Bob" in t.data.row_labels`.
181
+
182
+ ## The raw layer
183
+
184
+ For sheets that are not a labelled table — plain grids, exports, odd layouts.
185
+
186
+ ```python
187
+ from pyhandlexl import read_sheet, write_sheet, append_rows
188
+
189
+ read_sheet(path, sheet=None, *, pad=False)
190
+ ```
191
+
192
+ Returns `list[list[str]]`. Trailing empty cells are trimmed from each row (a
193
+ fully empty row becomes `[]`); `pad=True` right-pads every row to the widest
194
+ row's length instead.
195
+
196
+ ```python
197
+ write_sheet(path, rows, sheet=None, *, orientation="rows")
198
+ ```
199
+
200
+ Replaces the target sheet with `rows` (other sheets untouched), creating the
201
+ file and sheet if needed. Values are written as-is — `str` stays `str`, `int`
202
+ stays `int`, `None` leaves the cell empty; there is no string-to-number
203
+ conversion. `orientation="columns"` writes each inner list *down a column*
204
+ instead of across a row.
205
+
206
+ ```python
207
+ append_rows(path, rows, sheet=None)
208
+ ```
209
+
210
+ Appends after the last row. Empty input is a no-op.
211
+
212
+ ## Sheet management
213
+
214
+ ```python
215
+ from pyhandlexl import (
216
+ list_sheets, sheet_exists, create_sheet, delete_sheet, rename_sheet,
217
+ )
218
+
219
+ list_sheets(path) # ['Sheet1', 'Data']
220
+ sheet_exists(path, "Data") # True
221
+ create_sheet(path, "Results") # ValueError if it already exists
222
+ delete_sheet(path, "Old") # refuses to delete the last sheet
223
+ rename_sheet(path, "Old", "New")
224
+ ```
225
+
226
+ Sheet names are validated everywhere: max 31 characters, none of `\ / ? * [ ] :`,
227
+ and `"History"` is reserved by Excel.
228
+
229
+ ## Safe writes
230
+
231
+ Every write goes through the same steps:
232
+
233
+ 1. Save to a temporary file in the same directory.
234
+ 2. Verify it is a readable `.xlsx`.
235
+ 3. Atomically replace the original (`os.replace`).
236
+
237
+ If any step fails the temporary file is removed and the original is left exactly
238
+ as it was. If the target is locked (open in Excel), writes retry briefly before
239
+ raising `FileLockedError`.
240
+
241
+ ## Errors
242
+
243
+ All raised exceptions derive from `PyhandlexlError`:
244
+
245
+ | Exception | Also a | Meaning |
246
+ |---|---|---|
247
+ | `SheetNameError` | `ValueError` | invalid worksheet name |
248
+ | `DimensionError` | `ValueError` | data exceeds Excel's 1,048,576 × 16,384 grid |
249
+ | `SheetNotFoundError` | `KeyError` | no worksheet with that name |
250
+ | `FileLockedError` | `OSError` | file stayed locked through every retry |
251
+ | `InvalidFileError` | — | file is missing or not a readable `.xlsx` |
252
+
253
+ ## Not in scope
254
+
255
+ `pyhandlexl` deliberately does **not** handle: cell formatting, styles, fonts,
256
+ formulas, charts, images, merged cells, `.xls` (old format), or password
257
+ protection / encryption. For any of that, use openpyxl directly.
258
+
259
+ ## Development
260
+
261
+ ```bash
262
+ python -m venv .venv
263
+ source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
264
+ pip install -e ".[dev]"
265
+ pytest
266
+ ruff check . && ruff format --check .
267
+ ```
268
+
269
+ ## License
270
+
271
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyhandlexl"
7
+ version = "0.2.0"
8
+ description = "Use an Excel file as the database for your next project."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Lewis Wainaina", email = "lewyamendi@gmail.com" }]
14
+ keywords = ["excel", "xlsx", "openpyxl", "spreadsheet", "xl"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Office/Business :: Financial :: Spreadsheet",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = ["openpyxl>=3.1"]
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=8", "ruff>=0.6"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/LewyAmendi/pyhandlexl"
35
+ Repository = "https://github.com/LewyAmendi/pyhandlexl"
36
+ Issues = "https://github.com/LewyAmendi/pyhandlexl/issues"
37
+ Changelog = "https://github.com/LewyAmendi/pyhandlexl/blob/main/CHANGELOG.md"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.setuptools.package-data]
43
+ pyhandlexl = ["py.typed"]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py310"
48
+ src = ["src", "tests"]
49
+ # Leave Markdown to the author — keep README example formatting hand-aligned.
50
+ extend-exclude = ["*.md"]
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ addopts = "-ra"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,46 @@
1
+ """pyhandlexl — read and write raw cell values in Excel .xlsx files."""
2
+
3
+ from pyhandlexl.core import (
4
+ append_rows,
5
+ create_sheet,
6
+ delete_sheet,
7
+ list_sheets,
8
+ read_sheet,
9
+ rename_sheet,
10
+ sheet_exists,
11
+ write_sheet,
12
+ )
13
+ from pyhandlexl.errors import (
14
+ DimensionError,
15
+ FileLockedError,
16
+ InvalidFileError,
17
+ PyhandlexlError,
18
+ SheetNameError,
19
+ SheetNotFoundError,
20
+ )
21
+ from pyhandlexl.table import Table, TableData
22
+ from pyhandlexl.validate import check_dimensions, check_sheet_name, is_valid_xlsx
23
+
24
+ __version__ = "0.2.0"
25
+
26
+ __all__ = [
27
+ "DimensionError",
28
+ "FileLockedError",
29
+ "InvalidFileError",
30
+ "PyhandlexlError",
31
+ "SheetNameError",
32
+ "SheetNotFoundError",
33
+ "Table",
34
+ "TableData",
35
+ "append_rows",
36
+ "check_dimensions",
37
+ "check_sheet_name",
38
+ "create_sheet",
39
+ "delete_sheet",
40
+ "is_valid_xlsx",
41
+ "list_sheets",
42
+ "read_sheet",
43
+ "rename_sheet",
44
+ "sheet_exists",
45
+ "write_sheet",
46
+ ]