cellproof 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.
- cellproof-0.2.0/.github/workflows/ci.yml +29 -0
- cellproof-0.2.0/.github/workflows/publish.yml +48 -0
- cellproof-0.2.0/.gitignore +13 -0
- cellproof-0.2.0/AGENTS.md +46 -0
- cellproof-0.2.0/CHANGELOG.md +17 -0
- cellproof-0.2.0/DESIGN.md +117 -0
- cellproof-0.2.0/LICENSE +21 -0
- cellproof-0.2.0/PKG-INFO +177 -0
- cellproof-0.2.0/PLAN-2a.md +2347 -0
- cellproof-0.2.0/PLAN-2b.md +3345 -0
- cellproof-0.2.0/PLAN.md +2373 -0
- cellproof-0.2.0/PROGRESS.md +75 -0
- cellproof-0.2.0/README.md +143 -0
- cellproof-0.2.0/SECURITY.md +33 -0
- cellproof-0.2.0/docs/superpowers/plans/2026-10-14-plan-2c-speed.md +1360 -0
- cellproof-0.2.0/docs/superpowers/plans/2026-10-14-plan-2d-table-mode.md +1696 -0
- cellproof-0.2.0/docs/superpowers/specs/2026-10-14-plan-2c-speed-design.md +162 -0
- cellproof-0.2.0/docs/superpowers/specs/2026-10-14-plan-2d-table-mode-design.md +167 -0
- cellproof-0.2.0/docs/superpowers/specs/2026-10-18-running-totals-design.md +58 -0
- cellproof-0.2.0/docs/superpowers/specs/2026-10-19-lookups-row-wise-design.md +42 -0
- cellproof-0.2.0/examples/commission.xlsx +0 -0
- cellproof-0.2.0/examples/errors.xlsx +0 -0
- cellproof-0.2.0/examples/functions.xlsx +0 -0
- cellproof-0.2.0/examples/functions_unsaved.xlsx +0 -0
- cellproof-0.2.0/examples/make_commission.py +21 -0
- cellproof-0.2.0/examples/make_errors.py +26 -0
- cellproof-0.2.0/examples/make_functions.py +102 -0
- cellproof-0.2.0/pyproject.toml +48 -0
- cellproof-0.2.0/src/cellproof/__init__.py +1 -0
- cellproof-0.2.0/src/cellproof/__main__.py +3 -0
- cellproof-0.2.0/src/cellproof/cli.py +53 -0
- cellproof-0.2.0/src/cellproof/compiler.py +408 -0
- cellproof-0.2.0/src/cellproof/compute.py +62 -0
- cellproof-0.2.0/src/cellproof/emitter.py +160 -0
- cellproof-0.2.0/src/cellproof/expr.py +233 -0
- cellproof-0.2.0/src/cellproof/fenwick.py +47 -0
- cellproof-0.2.0/src/cellproof/fn/__init__.py +2 -0
- cellproof-0.2.0/src/cellproof/fn/aggregate.py +181 -0
- cellproof-0.2.0/src/cellproof/fn/criteria.py +188 -0
- cellproof-0.2.0/src/cellproof/fn/dates.py +171 -0
- cellproof-0.2.0/src/cellproof/fn/logic.py +131 -0
- cellproof-0.2.0/src/cellproof/fn/lookup.py +302 -0
- cellproof-0.2.0/src/cellproof/fn/maths.py +118 -0
- cellproof-0.2.0/src/cellproof/fn/text.py +257 -0
- cellproof-0.2.0/src/cellproof/functions.py +114 -0
- cellproof-0.2.0/src/cellproof/graph.py +150 -0
- cellproof-0.2.0/src/cellproof/model.py +72 -0
- cellproof-0.2.0/src/cellproof/parser.py +172 -0
- cellproof-0.2.0/src/cellproof/r1c1.py +202 -0
- cellproof-0.2.0/src/cellproof/rangeindex.py +97 -0
- cellproof-0.2.0/src/cellproof/reader.py +83 -0
- cellproof-0.2.0/src/cellproof/refs.py +58 -0
- cellproof-0.2.0/src/cellproof/region.py +392 -0
- cellproof-0.2.0/src/cellproof/regions.py +248 -0
- cellproof-0.2.0/src/cellproof/report.py +51 -0
- cellproof-0.2.0/src/cellproof/verifier.py +62 -0
- cellproof-0.2.0/tests/bench_shapes.py +90 -0
- cellproof-0.2.0/tests/test_aggregate.py +42 -0
- cellproof-0.2.0/tests/test_batches.py +109 -0
- cellproof-0.2.0/tests/test_cli.py +92 -0
- cellproof-0.2.0/tests/test_compiler.py +121 -0
- cellproof-0.2.0/tests/test_compute.py +62 -0
- cellproof-0.2.0/tests/test_criteria.py +65 -0
- cellproof-0.2.0/tests/test_dates.py +43 -0
- cellproof-0.2.0/tests/test_emitter.py +229 -0
- cellproof-0.2.0/tests/test_errors.py +92 -0
- cellproof-0.2.0/tests/test_excel_findings.py +102 -0
- cellproof-0.2.0/tests/test_excel_saved.py +58 -0
- cellproof-0.2.0/tests/test_fenwick.py +37 -0
- cellproof-0.2.0/tests/test_functions.py +52 -0
- cellproof-0.2.0/tests/test_graph.py +281 -0
- cellproof-0.2.0/tests/test_index.py +24 -0
- cellproof-0.2.0/tests/test_lexed.py +50 -0
- cellproof-0.2.0/tests/test_limits.py +27 -0
- cellproof-0.2.0/tests/test_logic.py +39 -0
- cellproof-0.2.0/tests/test_lookup.py +47 -0
- cellproof-0.2.0/tests/test_lookups.py +49 -0
- cellproof-0.2.0/tests/test_maths.py +30 -0
- cellproof-0.2.0/tests/test_parser.py +35 -0
- cellproof-0.2.0/tests/test_perf.py +56 -0
- cellproof-0.2.0/tests/test_r1c1.py +60 -0
- cellproof-0.2.0/tests/test_rangeindex.py +82 -0
- cellproof-0.2.0/tests/test_reader.py +105 -0
- cellproof-0.2.0/tests/test_refs.py +34 -0
- cellproof-0.2.0/tests/test_region.py +528 -0
- cellproof-0.2.0/tests/test_regions.py +130 -0
- cellproof-0.2.0/tests/test_regions_differential.py +55 -0
- cellproof-0.2.0/tests/test_regions_split.py +107 -0
- cellproof-0.2.0/tests/test_report.py +43 -0
- cellproof-0.2.0/tests/test_review_regressions.py +117 -0
- cellproof-0.2.0/tests/test_running.py +237 -0
- cellproof-0.2.0/tests/test_shapes.py +30 -0
- cellproof-0.2.0/tests/test_text.py +44 -0
- cellproof-0.2.0/tests/test_types.py +55 -0
- cellproof-0.2.0/tests/test_verifier.py +35 -0
- cellproof-0.2.0/tests/xlsxkit.py +125 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
17
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python }}
|
|
24
|
+
- name: Install
|
|
25
|
+
run: python -m pip install -e ".[dev,dbt]"
|
|
26
|
+
- name: Test
|
|
27
|
+
run: python -m pytest tests -q
|
|
28
|
+
env:
|
|
29
|
+
DBT_SEND_ANONYMOUS_USAGE_STATS: "False"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishing a GitHub release tagged vX.Y.Z uploads that version to PyPI.
|
|
4
|
+
# Uses PyPI trusted publishing: no token is stored anywhere.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
17
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- name: Tag must match the version in pyproject.toml
|
|
21
|
+
run: |
|
|
22
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
23
|
+
test "${GITHUB_REF_NAME}" = "v${version}" || { echo "tag ${GITHUB_REF_NAME} != v${version}"; exit 1; }
|
|
24
|
+
- name: Test
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install -e ".[dev]"
|
|
27
|
+
python -m pytest tests -q
|
|
28
|
+
- name: Build
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install build
|
|
31
|
+
python -m build
|
|
32
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: pypi
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# cellproof: guide for coding agents and contributors
|
|
2
|
+
|
|
3
|
+
The project was called `unexcel` until 0.2.0; the older plans and specs still use that name.
|
|
4
|
+
|
|
5
|
+
Read `README.md` first (what the tool does, limits), then `PROGRESS.md` (current state and next steps). `DESIGN.md` is
|
|
6
|
+
the original spec; `docs/superpowers/specs/` and `docs/superpowers/plans/` hold the design and plan of each later
|
|
7
|
+
piece. `PLAN*.md` are the early plans, kept for history.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
13
|
+
pip install -e ".[dev,dbt,compute]"
|
|
14
|
+
python -m pytest tests -q # dbt must be on PATH (activate the venv) or the dbt tests skip
|
|
15
|
+
CELLPROOF_PERF=1 python -m pytest tests/test_perf.py -s # 200k-formula benchmark
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Code map (`src/cellproof/`)
|
|
19
|
+
|
|
20
|
+
| Module | Job |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `model.py` | `Cell`, `Book`, `Verdict`, `Unsupported` (its message becomes the UNRESOLVED reason) |
|
|
23
|
+
| `reader.py` | loads the workbook twice with openpyxl (formulas, saved values); refuses zip bombs |
|
|
24
|
+
| `parser.py`, `refs.py` | formula text -> AST with Excel precedence; reference text -> `Area` |
|
|
25
|
+
| `r1c1.py` | how a reference behaves when a formula is pasted down (same row, absolute, previous row, growing range) |
|
|
26
|
+
| `regions.py` | finds regions (columns repeating one formula over the same rows), running totals, splits on cycles |
|
|
27
|
+
| `graph.py`, `fenwick.py`, `rangeindex.py` | dependency order without expanding ranges; cycle detection |
|
|
28
|
+
| `expr.py` | `Expr` (SQL value + SQL error code), Excel coercions |
|
|
29
|
+
| `compiler.py` | cell mode: formula -> SQL in batches |
|
|
30
|
+
| `region.py` | a region -> one set-based statement (window functions for running totals) |
|
|
31
|
+
| `functions.py`, `fn/` | the 93 Excel functions |
|
|
32
|
+
| `verifier.py` | runs everything on DuckDB and compares with Excel's saved values |
|
|
33
|
+
| `emitter.py` | writes the dbt project; `--dbt` runs `dbt build` |
|
|
34
|
+
| `report.py`, `cli.py`, `compute.py` | REPORT.md/report.json, the CLI, the optional `formulas` answer key |
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
- **Never guess a value.** Anything not translated exactly raises `Unsupported("<reason a user understands>")`.
|
|
39
|
+
- **A false PASS is the worst bug.** When unsure, produce an honest FAIL or UNRESOLVED.
|
|
40
|
+
- **No DuckDB exception may escape from a supported function.** Guard every domain with `CASE`.
|
|
41
|
+
- **Regions must equal cell mode.** `compile_book(book, regions=False)` is the reference; region tests compare against it.
|
|
42
|
+
- **Runtime dependencies stay `openpyxl` and `duckdb`.** Anything else goes in an optional extra.
|
|
43
|
+
- **Cell values never leave the machine.** No network calls with workbook data.
|
|
44
|
+
- **Tests first**, console output ASCII-only (Windows consoles), measure before claiming speed.
|
|
45
|
+
- **Real Excel is the ground truth**; the `formulas` library is only a labelled fallback answer key.
|
|
46
|
+
- Commits made by an agent end with a `Co-Authored-By:` trailer for the agent.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
- **Renamed** from `unexcel` to `cellproof` (the old name clashed with an R package): `pip install cellproof`, command
|
|
6
|
+
`cellproof convert`, import `cellproof`, `CELLPROOF_PERF` for the benchmark, dbt project and database `cellproof`.
|
|
7
|
+
- **Lookups row-wise:** VLOOKUP, HLOOKUP, MATCH, INDEX and XLOOKUP pasted down a column compile into one statement.
|
|
8
|
+
1,000 VLOOKUPs: verify 5.7 s -> 0.1 s.
|
|
9
|
+
- **Criteria row-wise:** COUNTIFS (and SUMIF/AVERAGEIF(S) over a range holding an error) with a criterion from the row
|
|
10
|
+
no longer fall back to cell mode. 1,000 COUNTIFS: verify 29 s -> 0.1 s.
|
|
11
|
+
- **Faster compile:** a pasted-down formula is tokenized once per run. 200k-formula benchmark 31 s -> 26 s; 50k
|
|
12
|
+
VLOOKUPs 8.9 s -> 4.6 s.
|
|
13
|
+
- A lookup outside a region is about 20% slower than in 0.1.0 (one SQL form for both modes).
|
|
14
|
+
|
|
15
|
+
## 0.1.0 (2026-10-18)
|
|
16
|
+
|
|
17
|
+
First release, as `unexcel`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# unexcel — Design (v1)
|
|
2
|
+
|
|
3
|
+
> Status: **ACCEPTED 2026-10-11** (architecture, error handling, testing validated with user).
|
|
4
|
+
> Working name. `unexcel` clashes with a CRAN R package / `drhrf/unexcel` repo — rename before launch.
|
|
5
|
+
> Date: 2026-10-11
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Understanding summary
|
|
10
|
+
|
|
11
|
+
- **What:** `pip install` CLI. Input: Excel workbook (`.xlsx` / `.xlsm`). Output: dbt project that recomputes the workbook's numbers + a report proving every output cell matches.
|
|
12
|
+
- **Why:** Business logic trapped in workbooks one person understands. Manual migration takes weeks, and nobody proves the numbers still match. Verified 2026-10-11: no open-source tool compiles Excel *formulas* to SQL/dbt. Existing tools copy *data*, not logic.
|
|
13
|
+
- **Who:** Analysts, finance/ops teams, analytics engineers moving off Excel.
|
|
14
|
+
- **Requirements from the user:** runs in anyone's environment, fast, reliable.
|
|
15
|
+
- **Non-goals (v1):** VBA/macros, pivot tables, Power Query, charts, external links, Power BI output, web UI, `.xls`/`.xlsb`.
|
|
16
|
+
|
|
17
|
+
## 2. Assumptions
|
|
18
|
+
|
|
19
|
+
1. Workbooks up to ~50 sheets / ~200k formula cells. Target: under 5 min on a laptop. Measure it, don't promise it.
|
|
20
|
+
2. v1 supports the ~80 most common functions. Every unsupported cell is reported as `UNRESOLVED`, never silently skipped.
|
|
21
|
+
3. Match rule: numbers within 1e-9 relative, text exact, blank vs. 0 vs. error compared explicitly.
|
|
22
|
+
4. Answer key = the cached values Excel saved in the file. If they're missing (file written by a script), compute them with the `formulas` library and label them `computed`, not `excel-saved`.
|
|
23
|
+
5. Deterministic: same workbook in, same output out. AI answers are cached on disk.
|
|
24
|
+
6. Solo maintainer, 4 weeks × ~15 h. Interview 5 analysts first; continue if at least 2 want to try it.
|
|
25
|
+
|
|
26
|
+
## 3. Decision log
|
|
27
|
+
|
|
28
|
+
| # | Decision | Alternatives | Why |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| D1 | Build unexcel | MetricProof, sqlport, whydrop-crew, pbi-doctor, onemetric | Verified gap, exact answer key, biggest audience |
|
|
31
|
+
| D2 | Output a dbt project | plain SQL, pandas, PBI model | What teams ship; dbt runs on every warehouse |
|
|
32
|
+
| D3 | CLI core, Hermes optional | Hermes required; two repos | Anyone can try it in a minute |
|
|
33
|
+
| D4 | Rules first, AI for the rest | LLM-first; no AI | Fast, repeatable, free |
|
|
34
|
+
| D5 | AI sees structure only | full values; opt-in flag | Finance data never leaves the machine |
|
|
35
|
+
| D6 | Laya (local, Apache-2.0) default; Jev optional | Jev default | Jev is a closed, early-access cloud API |
|
|
36
|
+
| D7 | Max 3 retries + no-progress stop + `UNRESOLVED` | max 5; loop until pass | Bounded and honest |
|
|
37
|
+
| D8 | **Approach A:** deterministic compiler + thin captain loop on failures; cell-mode (C) fallback for non-table regions | B agent-native; C cell-only | B fails fast/reliable/portable; C alone gives unreadable SQL |
|
|
38
|
+
| D9 | Verify loop on DuckDB directly; `dbt build` once at the end | dbt on every retry | dbt start-up costs seconds per call; the loop must be fast |
|
|
39
|
+
|
|
40
|
+
## 4. Architecture
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
.xlsx ─► reader ─► graph ─► regions ─► compiler ─► emitter ─► verifier ─► report
|
|
44
|
+
▲ ▲ │
|
|
45
|
+
│ └──── captain ◄────────┘ (failed / unsupported cells)
|
|
46
|
+
└── Decider adapter (Laya/Jev) └── Translator adapter (any LLM)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Plain-code pipeline. The captain only wakes for cells the rules can't handle. Agents get narrow, typed jobs.
|
|
50
|
+
|
|
51
|
+
### Components (one module each)
|
|
52
|
+
|
|
53
|
+
| Module | Job | Built on |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `reader` | Load the workbook twice: formulas + cached values. Resolve named ranges. List hidden sheets and merged cells. | `openpyxl` |
|
|
56
|
+
| `graph` | Tokenize formulas → cell dependency graph → topological order. Circular refs → `UNRESOLVED`. | `openpyxl.formula.Tokenizer`, stdlib `graphlib` |
|
|
57
|
+
| `regions` | Find tables: contiguous block, header row, same formula shape down a column (R1C1-equal). Ambiguous → ask the Decider. Low confidence → abstain → cell mode. | rules + Decider |
|
|
58
|
+
| `compiler` | Formula AST → SQL expression. One registry entry per function (`SUMIFS` → grouped aggregate/join, `IF` → `CASE`, `VLOOKUP`/`XLOOKUP` → left join). | `sqlglot` expression builders |
|
|
59
|
+
| `emitter` | Write the dbt project: input tables as seeds, one model per region, `schema.yml` tests, docs, lineage. Transpile to the target dialect. | `sqlglot`, Jinja-free string templates |
|
|
60
|
+
| `verifier` | Run the compiled SQL on DuckDB and map result rows/cols back to cell addresses. Compare with the answer key → `PASS` / `FAIL(evidence)` / `UNRESOLVED(reason)`. | `duckdb` |
|
|
61
|
+
| `captain` | For each failed group: build a task {formula, neighbour schema, failing evidence} → Translator → recompile → re-verify **only that group**. Max 3 rounds; stop if a round fixes nothing. | plain Python loop |
|
|
62
|
+
| `report` | `REPORT.md` + `report.json`: pass/fail/unresolved counts, per-cell status, findings. | stdlib |
|
|
63
|
+
| `adapters` | `Decider` (choice + probability, can abstain): Laya local default, Jev optional. `Translator`: one HTTP call to any LLM. Installed only via extras. | `unexcel[laya]`, `unexcel[llm]` |
|
|
64
|
+
|
|
65
|
+
Hermes: separate optional skill that shells out to `unexcel convert`, returns the zip + report over Telegram. Not in the core package.
|
|
66
|
+
|
|
67
|
+
## 5. Data flow (one run)
|
|
68
|
+
|
|
69
|
+
1. `unexcel convert model.xlsx --out model_dbt/ [--target snowflake]`
|
|
70
|
+
2. `reader` → in-memory cells (formula, cached value, type).
|
|
71
|
+
3. `graph` → order + cycles.
|
|
72
|
+
4. `regions` → tables + leftover cells (cell mode).
|
|
73
|
+
5. `compiler` → SQL per region; unsupported cells queued.
|
|
74
|
+
6. `verifier` → per-cell verdicts on DuckDB (seconds).
|
|
75
|
+
7. `captain` → retries on queued/failed groups (≤3 rounds).
|
|
76
|
+
8. `emitter` → dbt project; final `dbt build` on DuckDB as an end-to-end check.
|
|
77
|
+
9. `report` → summary printed; the user reviews `REPORT.md` (the human gate). Nothing is pushed anywhere.
|
|
78
|
+
|
|
79
|
+
Privacy: only formula text, headers, sheet names and types go to the adapters. Cell values never leave the process.
|
|
80
|
+
|
|
81
|
+
## 6. Error handling & edge cases
|
|
82
|
+
|
|
83
|
+
| Case | Behaviour |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `.xls`, `.xlsb`, encrypted, corrupt | Clear error naming the format; exit code 2 |
|
|
86
|
+
| Volatile `NOW`, `TODAY`, `RAND`, `OFFSET`, `INDIRECT` | `UNRESOLVED` (non-deterministic or dynamic reference) |
|
|
87
|
+
| External links `[Book2.xlsx]` | `UNRESOLVED` |
|
|
88
|
+
| Circular references | `UNRESOLVED`, cycle listed |
|
|
89
|
+
| Excel errors `#N/A`, `#DIV/0!` | SQL `NULL` + error-kind column; compared by kind |
|
|
90
|
+
| Numbers stored as text | Copy Excel semantics (`SUMIFS` skips them) + **finding** in the report |
|
|
91
|
+
| Hard-coded value inside a formula column | Kept as an `overrides` seed + **finding** |
|
|
92
|
+
| Dates | Excel serial → date with the 1900 leap-year bug (serial 60) handled; 1904 date system detected |
|
|
93
|
+
| Whole-column refs `C:C` | Bound to the detected table's rows |
|
|
94
|
+
| Merged cells, hidden sheets | Read normally; listed in the report |
|
|
95
|
+
| No cached values | Compute with `formulas`, label `computed` |
|
|
96
|
+
|
|
97
|
+
## 7. Testing strategy
|
|
98
|
+
|
|
99
|
+
- **Fixtures:** small workbooks, one per function/edge case. Cached values come from a real recalculation (LibreOffice headless `--convert-to xlsx`), not hand-typed. LibreOffice differs from Excel on some functions, so a core set of ~20 fixtures is saved once in real Excel and treated as ground truth.
|
|
100
|
+
- **Golden test:** for every fixture, `convert` → every cell `PASS` or the expected `UNRESOLVED`.
|
|
101
|
+
- **Unit tests:** compiler registry (formula → SQL), region detection, date conversion.
|
|
102
|
+
- **Rules-only baseline:** the full suite passes with no AI installed. An AI adapter stays only if it measurably lowers `UNRESOLVED` on the fixtures.
|
|
103
|
+
- **Perf check:** one generated 200k-formula workbook, timed in CI.
|
|
104
|
+
- **CI:** GitHub Actions on Windows, macOS, Linux × Python 3.10–3.13.
|
|
105
|
+
|
|
106
|
+
## 8. Risks
|
|
107
|
+
|
|
108
|
+
1. Excel function long tail (500+) → mitigated by honest `UNRESOLVED` + a contributor-friendly function registry.
|
|
109
|
+
2. Region detection on messy sheets → cell-mode fallback; Decider abstains instead of guessing.
|
|
110
|
+
3. Dialect drift (verified on DuckDB, shipped as Snowflake/BigQuery) → `--verify-on target` (post-v1).
|
|
111
|
+
4. Name clash → rename before the PyPI publish.
|
|
112
|
+
|
|
113
|
+
## 9. Open questions
|
|
114
|
+
|
|
115
|
+
- License: MIT or Apache-2.0?
|
|
116
|
+
- Demo workbook: a public financial model, or build a messy realistic one?
|
|
117
|
+
- Final name.
|
cellproof-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nikhil414 and cellproof contributors
|
|
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.
|
cellproof-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cellproof
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Turn an Excel workbook into SQL (a dbt project on DuckDB) and prove every number matches.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Nikhil414/cellproof
|
|
6
|
+
Project-URL: Issues, https://github.com/Nikhil414/cellproof/issues
|
|
7
|
+
Author: Nikhil414
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: dbt,duckdb,excel,migration,spreadsheet,sql,xlsx
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: duckdb>=1.1
|
|
25
|
+
Requires-Dist: openpyxl>=3.1
|
|
26
|
+
Provides-Extra: compute
|
|
27
|
+
Requires-Dist: formulas<1.4,>=1.3.4; extra == 'compute'
|
|
28
|
+
Provides-Extra: dbt
|
|
29
|
+
Requires-Dist: dbt-duckdb>=1.8; extra == 'dbt'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: jinja2>=3.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# cellproof
|
|
36
|
+
|
|
37
|
+
**Turn an Excel workbook into SQL, and prove every number matches.**
|
|
38
|
+
|
|
39
|
+
(Released as `unexcel` up to 0.1.0; renamed to cellproof in 0.2.0.)
|
|
40
|
+
|
|
41
|
+
Business logic often lives in a spreadsheet that one person understands. Moving it into a database is risky because
|
|
42
|
+
nobody can show the new numbers equal the old ones. cellproof translates every formula of an `.xlsx` into SQL, runs that
|
|
43
|
+
SQL on [DuckDB](https://duckdb.org), and compares each result with the value Excel itself saved in the file. Excel's
|
|
44
|
+
saved values are a free answer key.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
$ cellproof convert examples/functions.xlsx
|
|
48
|
+
72 pass, 0 fail, 4 unresolved -> examples/functions_dbt/REPORT.md
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Every formula cell gets one verdict:
|
|
52
|
+
|
|
53
|
+
| Verdict | Meaning |
|
|
54
|
+
|---|---|
|
|
55
|
+
| **PASS** | the SQL gives the value Excel saved |
|
|
56
|
+
| **FAIL** | the SQL gives a different value; the report shows both |
|
|
57
|
+
| **UNRESOLVED** | cellproof could not translate the formula exactly; the report says why. It never guesses. |
|
|
58
|
+
|
|
59
|
+
Exit code: `0` no FAIL, `1` at least one FAIL (or `dbt build` failed), `2` the workbook could not be read, `3`
|
|
60
|
+
DuckDB rejected SQL that cellproof generated (a bug in cellproof; the message names the cell, please report it).
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
Python 3.10 or newer.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install cellproof
|
|
68
|
+
pip install "cellproof[dbt]" # also run dbt build (--dbt)
|
|
69
|
+
pip install "cellproof[compute]" # answer key for files never saved in Excel
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Runtime dependencies are only `openpyxl` and `duckdb`.
|
|
73
|
+
|
|
74
|
+
## Use
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cellproof convert book.xlsx # writes book_dbt/ next to the file
|
|
78
|
+
cellproof convert book.xlsx --out out_dir # choose the folder
|
|
79
|
+
cellproof convert book.xlsx --dbt # also run `dbt build` on the generated project
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The output folder holds:
|
|
83
|
+
|
|
84
|
+
- **`REPORT.md` / `report.json`**: every verdict, failed cells with both values, unresolved cells with the reason,
|
|
85
|
+
findings (for example a number stored as text, which Excel skips in SUM), and which blocks of rows became readable
|
|
86
|
+
table models.
|
|
87
|
+
- **A dbt project** (`dbt_project.yml`, `models/`, `seeds/input_cells.csv`) that recomputes the workbook on DuckDB.
|
|
88
|
+
It runs exactly the SQL the verifier checked.
|
|
89
|
+
|
|
90
|
+
The workbook must have been saved by Excel (so the file holds Excel's values). For a file generated by code and never
|
|
91
|
+
opened in Excel, install the `compute` extra: the [`formulas`](https://pypi.org/project/formulas/) library then
|
|
92
|
+
computes the expected values, and the report labels them `computed` (a weaker answer key than Excel).
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
reader -> regions -> dependency graph -> compiler -> verifier -> dbt emitter -> report
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
1. **Read** the file twice with openpyxl: the formulas, and the values Excel saved. Dates become Excel serial numbers.
|
|
101
|
+
2. **Find regions.** A block of rows where columns repeat one formula down (`=A2*1.1`, `=IF(B2>100,B2,0)`) is a
|
|
102
|
+
*region*. Running totals in a region (`=C1+B2`, `=SUM($B$2:B2)`) become SQL window functions. A region compiles to
|
|
103
|
+
one readable statement (`amount * 1.1 AS tax`) instead of one statement per cell.
|
|
104
|
+
3. **Order** every formula after the cells it reads. Circular references are UNRESOLVED.
|
|
105
|
+
4. **Compile** each formula to SQL with Excel's rules: Excel's type conversions (`"3"+1 = 4`), its error codes
|
|
106
|
+
(`#DIV/0!`, `#N/A`, `#VALUE!` ...) which must match exactly, the left-to-right order in which Excel reports errors,
|
|
107
|
+
and DuckDB guards (division by zero, `ln(0)`, overflow of `POWER`) so DuckDB never fails where Excel returns an
|
|
108
|
+
error. Cells outside regions run in batches ("cell mode").
|
|
109
|
+
5. **Verify** on DuckDB in memory: numbers must match within a relative 1e-9, text and booleans exactly, errors by
|
|
110
|
+
code.
|
|
111
|
+
6. **Emit** the dbt project and the report.
|
|
112
|
+
|
|
113
|
+
93 Excel functions are supported: ABS, AND, AVERAGE, AVERAGEIF(S), CEILING, CHAR, CHOOSE, CLEAN, CONCAT(ENATE),
|
|
114
|
+
COUNT, COUNTA, COUNTBLANK, COUNTIF(S), DATE, DATEDIF, DAY, DAYS, DAYS360, EDATE, EOMONTH, EXACT, EXP, FIND, FLOOR,
|
|
115
|
+
HLOOKUP, IF, IFERROR, IFNA, IFS, INDEX, INT, IS* (BLANK, ERR, ERROR, LOGICAL, NA, NUMBER, TEXT), LEFT, LEN, LN, LOG,
|
|
116
|
+
LOG10, LOWER, MATCH, MAX, MAXIFS, MEDIAN, MID, MIN, MINIFS, MOD, MONTH, MROUND, NA, NOT, OR, PI, POWER, PRODUCT,
|
|
117
|
+
PROPER, REPLACE, REPT, RIGHT, ROUND(UP/DOWN), SEARCH, SIGN, SQRT, SUBSTITUTE, SUBTOTAL, SUM, SUMIF(S), SUMPRODUCT,
|
|
118
|
+
SWITCH, TEXT, TEXTJOIN, TRIM, TRUE/FALSE, TRUNC, UPPER, VALUE, VLOOKUP, WEEKDAY, XLOOKUP, XOR, YEAR.
|
|
119
|
+
|
|
120
|
+
**Speed** (Windows 11 laptop, measured): a 200,000-formula workbook (four formula columns over 50,000 rows, one of them
|
|
121
|
+
a running total) converts and verifies in 26 s, all PASS. 50,000 rows of VLOOKUP into a 1,000-row table: 4.6 s.
|
|
122
|
+
COUNTIFS/SUMIFS with a criterion from the row run at the same speed.
|
|
123
|
+
|
|
124
|
+
## Limitations
|
|
125
|
+
|
|
126
|
+
cellproof prefers an honest UNRESOLVED or FAIL to a guess. Known limits, each reported with a reason:
|
|
127
|
+
|
|
128
|
+
- **File formats:** `.xlsx` and `.xlsm` only (no `.xls`, `.xlsb`, `.ods`). Macros (VBA) are ignored.
|
|
129
|
+
- **Not supported:** array formulas and dynamic arrays, volatile functions (`NOW`, `TODAY`, `RAND`, `OFFSET`,
|
|
130
|
+
`INDIRECT`), functions outside the list above, external workbooks, structured table references (`Table1[@Col]`),
|
|
131
|
+
INDEX used as a reference, the 1904 date system in TEXT date formats.
|
|
132
|
+
- **Approximate lookups on unsorted data** (MATCH/VLOOKUP/HLOOKUP with match type 1/-1): UNRESOLVED, because Excel's
|
|
133
|
+
answer then depends on its binary search.
|
|
134
|
+
- **Text to number:** only plain numbers convert (`"3"`, `" -1.5e3 "`). Excel also reads `"1,000"`, `"$5"`, `"50%"`;
|
|
135
|
+
cellproof gives `#VALUE!`, which shows as a FAIL.
|
|
136
|
+
- **Some function options:** SUBTOTAL 101-111 (hidden rows), DATEDIF units MD/YM/YD, WEEKDAY types 11-17, TEXT formats
|
|
137
|
+
outside the supported table, COUNT or MEDIAN of values typed into the formula, criteria taken from an empty cell.
|
|
138
|
+
- **Floating point:** a running total becomes a fast window function only when Excel's saved values prove the different
|
|
139
|
+
addition order cannot matter; balances that cancel back to zero (ledgers) are added in Excel's order instead, which
|
|
140
|
+
is slower. Elsewhere SQL can still differ from Excel in the last digits, which would show as an honest FAIL. A
|
|
141
|
+
number that overflows becomes `inf` instead of `#NUM!`.
|
|
142
|
+
- **Speed:** regions cover columns that repeat one formula over the same rows, lookups and *IF(S) criteria included.
|
|
143
|
+
A formula that is not part of such a column runs cell by cell (a lookup alone: about 7 s per 1,000). Running totals
|
|
144
|
+
other than plain sums (`=C1*1.05+B2`, `=MAX(C1,B2)`) also run cell by cell.
|
|
145
|
+
- **The answer key:** a workbook must have been saved by Excel; `formulas` (the `compute` extra) is a weaker fallback
|
|
146
|
+
and is known to disagree with Excel in places.
|
|
147
|
+
- **dbt:** the generated project targets DuckDB (`dbt-duckdb`). Other warehouses are not generated yet.
|
|
148
|
+
|
|
149
|
+
## Security and privacy
|
|
150
|
+
|
|
151
|
+
- **No network.** cellproof never sends workbook data anywhere; everything runs locally in DuckDB.
|
|
152
|
+
- **Untrusted files:** workbook text only ever reaches SQL as escaped string literals; headers become SQL names only
|
|
153
|
+
after being reduced to `[a-z0-9_]`; braces are escaped so dbt's Jinja cannot run workbook text. A file that unzips to
|
|
154
|
+
more than 2 GB is refused. For extra XML hardening on untrusted files, `pip install defusedxml` (openpyxl then uses it
|
|
155
|
+
automatically).
|
|
156
|
+
- **Output folder:** a rerun deletes the old `*.sql` files in `<out>/models/cellproof_cells/` so stale models cannot
|
|
157
|
+
linger; point `--out` at a folder of its own.
|
|
158
|
+
- **`--dbt`** runs your installed `dbt` on the generated project (no shell).
|
|
159
|
+
|
|
160
|
+
See [SECURITY.md](SECURITY.md) to report a vulnerability.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
166
|
+
pip install -e ".[dev,dbt,compute]"
|
|
167
|
+
python -m pytest tests -q # ~8 min with dbt on PATH
|
|
168
|
+
CELLPROOF_PERF=1 python -m pytest tests/test_perf.py -s # the 200k-formula benchmark
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Rules the code follows: never guess a value (anything not translated exactly is UNRESOLVED with a reason a user
|
|
172
|
+
understands); a false PASS is the worst bug; no DuckDB exception may escape from a supported function; tests first; real
|
|
173
|
+
Excel is the ground truth. Design notes: [DESIGN.md](DESIGN.md) and `docs/superpowers/specs/`.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
[MIT](LICENSE)
|