A lightweight Excel ORM for generating templates and parsing typed row models.
Project description
Excel ORM
A lightweight, typed “Excel ORM” for generating Excel templates and parsing Excel workbooks into Python objects using column descriptors.
This project is designed for the common enterprise pattern where you:
- generate a structured
.xlsxtemplate for users, - let users fill it in,
- load the workbook back into Python, producing typed objects grouped by model.
It uses openpyxl for reading/writing Excel files and supports:
- multiple model “tables” laid out horizontally on a worksheet, and
- pivot-style sheets (matrix input) that expand into row objects.
Features
-
Typed column descriptors (
text_column,int_column,bool_column,date_column) -
Template generation for standard tables:
- merged table title cells (pluralized model name)
- bold headers
- sensible column widths
- multiple tables laid out horizontally on the same sheet with a configurable gap
-
Template generation for pivot sheets:
- merged title across the row header block + pivot header span
- bold row-field headers in the left block (
row_fields) - pivot column headers rendered across the top (
pivot_values) - optional seeding of row keys down the left block (
row_values)
-
Workbook parsing into model-specific repositories:
excel_file.cars.all()→list[Car]excel_file.manufacturing_plants.all()→list[ManufacturingPlant]excel_file.demands.all()→list[Demand](from pivot input)
-
Validation hooks
- column-level
not_null - optional row exclusion rules via
excludes - optional model-level
validate()method
- column-level
Installation
From PyPI
pip install excel-orm
From source (uv)
git clone <your-repo-url>
cd excel-orm
uv sync
Quick Start
1) Define models using Column[...] descriptors
from excel_orm.column import Column, text_column, int_column
from excel_orm import ExcelFile, SheetSpec
class Car:
make: Column[str] = text_column(header="Make", not_null=True)
model: Column[str] = text_column(header="Model", not_null=True)
year: Column[int] = int_column(header="Year", not_null=True)
class ManufacturingPlant:
name: Column[str] = text_column(header="Factory Name", not_null=True)
location: Column[str] = text_column(header="Location")
2) Declare a sheet containing multiple models (standard tables)
Each model becomes its own table on the same worksheet.
sheet = SheetSpec(
name="Cars",
models=[Car, ManufacturingPlant],
title_row=1,
header_row=2,
data_start_row=3,
template_table_gap=2,
)
3) Create an ExcelFile, generate a template, then load data
excel_file = ExcelFile(sheets=[sheet])
excel_file.generate_template("car_inventory_template.xlsx")
# Users fill in data in Excel...
excel_file.load_data("car_inventory_data.xlsx")
cars = excel_file.cars.all()
plants = excel_file.manufacturing_plants.all()
print(cars[0].make, cars[0].year)
print(plants[0].name, plants[0].location)
Pivot Sheets
Pivot sheets are useful when users prefer matrix-style input (e.g., values by multiple row keys × time), but your application wants row objects.
Pivot Layout Summary
For a pivot sheet:
row_fieldsdefine the leftmost columns (starting atrow_header_col)pivot_valuesare written across the top row (header_row) starting atdata_start_coldata_start_colis computed as:
data_start_col = len(row_fields) + 1
Parsing behavior:
- pivot headers are read from
header_rowacross the top until the first blank header cell - parsing stops when the row header block (all
row_fieldscolumns) is blank for a row - each non-blank pivot cell emits one object (unless
include_blanks=True)
1) Define a “row” model
from datetime import date
from excel_orm.column import Column, text_column, date_column, int_column
class Demand:
region: Column[str] = text_column(header="Region", not_null=True)
product: Column[str] = text_column(header="Product", not_null=True)
dt: Column[date] = date_column(header="Date", not_null=True)
value: Column[int] = int_column(header="Value", not_null=True)
2) Create a PivotSheetSpec (multi-row-fields)
from datetime import date
from excel_orm import ExcelFile, PivotSheetSpec
spec = PivotSheetSpec(
name="Demand",
model=Demand,
pivot_field="dt",
row_fields=["region", "product"],
value_field="value",
pivot_values=[date(2025, 6, 1), date(2025, 7, 1), date(2025, 8, 1)],
# optional: seed row keys (must match row_fields arity)
row_values=[
("NA", "SKU-1"),
("NA", "SKU-2"),
("EU", "SKU-1"),
],
)
xf = ExcelFile(sheets=[spec])
xf.generate_template("demand_template.xlsx")
3) Load a filled pivot sheet
xf.load_data("demand_filled.xlsx")
rows = xf.demands.all()
# each element is a Demand row:
# Demand(region="NA", product="SKU-1", dt=date(2025, 6, 1), value=10)
row_values seeding rules
-
If
len(row_fields) == 1, eachrow_valueselement may be a single scalar value (backward compatible). -
If
len(row_fields) > 1, eachrow_valueselement must be a tuple/list with length == len(row_fields).- Otherwise template generation raises
ValueError.
- Otherwise template generation raises
How It Works
Repositories
For each model you register, ExcelFile creates a repository attribute on the instance using a snake_case pluralized name:
Car→excel_file.carsManufacturingPlant→excel_file.manufacturing_plantsDemand→excel_file.demands
Repositories are list-like containers with an all() helper:
cars = excel_file.cars.all() # list[Car]
Multi-table Sheets (standard tables)
A single worksheet can host multiple model tables. During template generation:
- a merged title cell is written above each table (pluralized class name in title case)
- headers appear under the title
- data rows begin at
data_start_row - tables are placed horizontally with
template_table_gapblank columns between them
During parsing:
- the library locates each model table by matching the expected header sequence
- it reads contiguous rows until a blank row is encountered
Column Types
Text
from excel_orm.column import Column, text_column
class Example:
name: Column[str] = text_column(header="Name", not_null=True, strip=True)
Noneparses to""(empty string)strip=Truetrims whitespace
Integer
from excel_orm.column import Column, int_column
class Example:
qty: Column[int] = int_column(header="Qty", not_null=True)
Noneor""parses to0
Boolean
from excel_orm.column import Column, bool_column
class Example:
active: Column[bool] = bool_column(header="Active")
Accepted values include:
- True:
true, t, yes, y, 1(case-insensitive) - False:
false, f, no, n, 0 None/ empty parses toFalse
Invalid values raise ValueError.
Date
from datetime import date
from excel_orm.column import Column, date_column
class Example:
start_date: Column[date] = date_column(header="Start Date")
The date parser supports:
- Excel-native
datetime/datevalues fromopenpyxl - ISO strings like
2025-06-01and2025-06-01T13:45:00 - common business formats including
01-JUN-2025
Invalid/empty values raise ValueError.
Validation
Column-level: not_null
class Car:
make: Column[str] = text_column(header="Make", not_null=True)
If a not_null=True column parses to None or "", a ValueError is raised.
Row exclusion: excludes
If you set excludes, rows matching those raw values in that column will be skipped during parsing.
status: Column[str] = text_column(header="Status")
status.spec.excludes = {"IGNORE", "SKIP"}
Model-level: validate()
If your model defines a validate(self) method, it is called after a row is parsed.
class Car:
make: Column[str] = text_column(header="Make", not_null=True)
year: Column[int] = int_column(header="Year", not_null=True)
def validate(self) -> None:
if self.year < 1886:
raise ValueError("Invalid car year")
Development
Run tests
uv run pytest
Lint/format
If you use Ruff:
uv run ruff check .
uv run ruff format .
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file excel_orm-0.1.5.tar.gz.
File metadata
- Download URL: excel_orm-0.1.5.tar.gz
- Upload date:
- Size: 42.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b114e6ba21b044d0015c4b081b9aa78fb5cacf89ad917a873ae906f9e4b55ba1
|
|
| MD5 |
7e11a25b774188513e4cd64fe1b9db6c
|
|
| BLAKE2b-256 |
c0ebb811861f2484c8f8c625ca6f28a2afae13f55fd7ac62a90c5c1639d994d1
|
File details
Details for the file excel_orm-0.1.5-py3-none-any.whl.
File metadata
- Download URL: excel_orm-0.1.5-py3-none-any.whl
- Upload date:
- Size: 10.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd7a47bcdd8546df0a66c3765e36119c88c23082a8732742d18b5629c71d46e2
|
|
| MD5 |
bdd816db250d011053aeb7a2f42b52c3
|
|
| BLAKE2b-256 |
d44a08e671912e8fb6ed583be27e8f5b75a553680423b6474db84a3a983f68d0
|