excel-orm 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.
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: excel-orm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Excel ORM for generating templates and parsing typed row models.
|
|
5
|
+
Project-URL: Homepage, https://github.com/acdelrusso/excel-orm
|
|
6
|
+
Project-URL: Repository, https://github.com/acdelrusso/excel-orm
|
|
7
|
+
Project-URL: Issues, https://github.com/acdelrusso/excel-orm/issues
|
|
8
|
+
Author: Anthony Del Russo
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: etl,excel,openpyxl,orm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Excel ORM
|
|
22
|
+
|
|
23
|
+
A lightweight, typed “Excel ORM” for generating Excel templates and parsing Excel workbooks into Python objects using column descriptors.
|
|
24
|
+
|
|
25
|
+
This project is designed for the common enterprise pattern where you:
|
|
26
|
+
1) generate a structured `.xlsx` template for users,
|
|
27
|
+
2) let users fill it in,
|
|
28
|
+
3) load the workbook back into Python, producing typed objects grouped by model.
|
|
29
|
+
|
|
30
|
+
It uses `openpyxl` for reading/writing Excel files and supports multiple model “tables” on the same worksheet.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Typed column descriptors** (`text_column`, `int_column`, `bool_column`, `date_column`)
|
|
37
|
+
- **Template generation** with:
|
|
38
|
+
- merged **table title cells** (pluralized model name)
|
|
39
|
+
- bold headers
|
|
40
|
+
- sensible column widths
|
|
41
|
+
- multiple tables laid out horizontally on the same sheet with a configurable gap
|
|
42
|
+
- **Workbook parsing** into model-specific repositories:
|
|
43
|
+
- `excel_file.cars.all()` → `list[Car]`
|
|
44
|
+
- `excel_file.manufacturing_plants.all()` → `list[ManufacturingPlant]`
|
|
45
|
+
- **Validation hooks**
|
|
46
|
+
- column-level `not_null`
|
|
47
|
+
- optional row exclusion rules via `excludes`
|
|
48
|
+
- optional model-level `validate()` method
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
### From PyPI (once published)
|
|
55
|
+
```bash
|
|
56
|
+
pip install excel-orm
|
|
57
|
+
````
|
|
58
|
+
|
|
59
|
+
### From source (uv)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone <your-repo-url>
|
|
63
|
+
cd excel-orm
|
|
64
|
+
uv sync
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
### 1) Define models using `Column[...]` descriptors
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from excel_orm.column import Column, text_column, int_column
|
|
75
|
+
from excel_orm.orm import ExcelFile, SheetSpec
|
|
76
|
+
|
|
77
|
+
class Car:
|
|
78
|
+
make: Column[str] = text_column(header="Make", not_null=True)
|
|
79
|
+
model: Column[str] = text_column(header="Model", not_null=True)
|
|
80
|
+
year: Column[int] = int_column(header="Year", not_null=True)
|
|
81
|
+
|
|
82
|
+
class ManufacturingPlant:
|
|
83
|
+
name: Column[str] = text_column(header="Factory Name", not_null=True)
|
|
84
|
+
location: Column[str] = text_column(header="Location")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2) Declare a sheet containing multiple models
|
|
88
|
+
|
|
89
|
+
Each model becomes its own table on the same worksheet.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
sheet = SheetSpec(
|
|
93
|
+
name="Cars",
|
|
94
|
+
models=[Car, ManufacturingPlant],
|
|
95
|
+
|
|
96
|
+
# Layout rows
|
|
97
|
+
title_row=1,
|
|
98
|
+
header_row=2,
|
|
99
|
+
data_start_row=3,
|
|
100
|
+
|
|
101
|
+
# Horizontal spacing between model tables
|
|
102
|
+
template_table_gap=2,
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3) Create an `ExcelFile`, generate a template, then load data
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
excel_file = ExcelFile(sheets=[sheet])
|
|
110
|
+
|
|
111
|
+
# Generate a blank template workbook
|
|
112
|
+
excel_file.generate_template("car_inventory_template.xlsx")
|
|
113
|
+
|
|
114
|
+
# Users fill in data in Excel...
|
|
115
|
+
|
|
116
|
+
# Load the filled workbook into repositories
|
|
117
|
+
excel_file.load_data("car_inventory_data.xlsx")
|
|
118
|
+
|
|
119
|
+
cars = excel_file.cars.all()
|
|
120
|
+
plants = excel_file.manufacturing_plants.all()
|
|
121
|
+
|
|
122
|
+
print(cars[0].make, cars[0].year)
|
|
123
|
+
print(plants[0].name, plants[0].location)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## How It Works
|
|
129
|
+
|
|
130
|
+
### Repositories
|
|
131
|
+
|
|
132
|
+
For each model you register, `ExcelFile` creates a repository attribute on the instance using a snake_case pluralized name:
|
|
133
|
+
|
|
134
|
+
* `Car` → `excel_file.cars`
|
|
135
|
+
* `ManufacturingPlant` → `excel_file.manufacturing_plants`
|
|
136
|
+
|
|
137
|
+
Repositories are simple list-like containers with an `all()` helper:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
cars = excel_file.cars.all() # list[Car]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Multi-table Sheets
|
|
144
|
+
|
|
145
|
+
A single worksheet can host multiple model tables. During template generation:
|
|
146
|
+
|
|
147
|
+
* A merged title cell is written above each table (pluralized class name in title case).
|
|
148
|
+
* Headers appear under the title.
|
|
149
|
+
* Data rows begin at `data_start_row`.
|
|
150
|
+
* Tables are placed horizontally with `template_table_gap` blank columns between them.
|
|
151
|
+
|
|
152
|
+
During parsing:
|
|
153
|
+
|
|
154
|
+
* The library locates each model table by matching the expected header sequence.
|
|
155
|
+
* It reads contiguous rows until a blank row is encountered.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Column Types
|
|
160
|
+
|
|
161
|
+
### Text
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from excel_orm.column import Column, text_column
|
|
165
|
+
|
|
166
|
+
class Example:
|
|
167
|
+
name: Column[str] = text_column(header="Name", not_null=True, strip=True)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
* `None` parses to `""` (empty string).
|
|
171
|
+
* `strip=True` trims whitespace.
|
|
172
|
+
|
|
173
|
+
### Integer
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from excel_orm.column import Column, int_column
|
|
177
|
+
|
|
178
|
+
class Example:
|
|
179
|
+
qty: Column[int] = int_column(header="Qty", not_null=True)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
* `None` or `""` parses to `0`.
|
|
183
|
+
|
|
184
|
+
### Boolean
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from excel_orm.column import Column, bool_column
|
|
188
|
+
|
|
189
|
+
class Example:
|
|
190
|
+
active: Column[bool] = bool_column(header="Active")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Accepted values include:
|
|
194
|
+
|
|
195
|
+
* True: `true, t, yes, y, 1` (case-insensitive)
|
|
196
|
+
* False: `false, f, no, n, 0`
|
|
197
|
+
* `None` / empty parses to `False`
|
|
198
|
+
|
|
199
|
+
Invalid values raise `ValueError`.
|
|
200
|
+
|
|
201
|
+
### Date
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from excel_orm.column import Column, date_column
|
|
205
|
+
|
|
206
|
+
class Example:
|
|
207
|
+
start_date: Column[date] = date_column(header="Start Date")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The date parser supports:
|
|
211
|
+
|
|
212
|
+
* Excel-native `datetime`/`date` values from `openpyxl`
|
|
213
|
+
* ISO strings like `2025-06-01` and `2025-06-01T13:45:00`
|
|
214
|
+
* Common business formats including `01-JUN-2025`
|
|
215
|
+
|
|
216
|
+
Invalid/empty values raise `ValueError`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Validation
|
|
221
|
+
|
|
222
|
+
### Column-level: `not_null`
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
class Car:
|
|
226
|
+
make: Column[str] = text_column(header="Make", not_null=True)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If a `not_null=True` column parses to `None` or `""`, a `ValueError` is raised.
|
|
230
|
+
|
|
231
|
+
### Row exclusion: `excludes`
|
|
232
|
+
|
|
233
|
+
If you set `excludes`, rows matching those raw values in that column will be skipped.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
status: Column[str] = text_column(header="Status")
|
|
237
|
+
status.spec.excludes = {"IGNORE", "SKIP"} # example pattern
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
(If you want a nicer API for excludes, consider adding it directly to the column factory signature.)
|
|
241
|
+
|
|
242
|
+
### Model-level: `validate()`
|
|
243
|
+
|
|
244
|
+
If your model defines a `validate(self)` method, it is called after a row is parsed.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
class Car:
|
|
248
|
+
make: Column[str] = text_column(header="Make", not_null=True)
|
|
249
|
+
year: Column[int] = int_column(header="Year", not_null=True)
|
|
250
|
+
|
|
251
|
+
def validate(self) -> None:
|
|
252
|
+
if self.year < 1886:
|
|
253
|
+
raise ValueError("Invalid car year")
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
### Run tests
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
uv run pytest
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Lint/format (example)
|
|
267
|
+
|
|
268
|
+
If you use Ruff:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
uv run ruff check .
|
|
272
|
+
uv run ruff format .
|
|
273
|
+
```
|