cellproof 0.2.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.
cellproof/report.py ADDED
@@ -0,0 +1,51 @@
1
+ """REPORT.md for people, report.json for tools."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from collections import Counter
6
+ from pathlib import Path
7
+
8
+ from cellproof.model import Key, Verdict, a1
9
+
10
+
11
+ def _md(value) -> str:
12
+ return "(no value)" if value is None else str(value).replace("|", "\\|")
13
+
14
+
15
+ def write_report(verdicts: list[Verdict], findings: list[str], out: Path, source: str,
16
+ computed: frozenset[Key] | set[Key] = frozenset(), notes=()) -> Counter:
17
+ counts = Counter({"PASS": 0, "FAIL": 0, "UNRESOLVED": 0})
18
+ counts.update(v.status for v in verdicts)
19
+ findings = list(dict.fromkeys(findings))
20
+
21
+ data = {
22
+ "source": source,
23
+ "counts": dict(counts),
24
+ "findings": findings,
25
+ "regions": list(notes),
26
+ "cells": [{"cell": a1(v.key), "status": v.status, "expected": v.expected, "actual": v.actual,
27
+ "reason": v.reason, "answer": "computed" if v.key in computed else "excel-saved"}
28
+ for v in verdicts],
29
+ }
30
+ (out / "report.json").write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
31
+
32
+ lines = [f"# cellproof report: {source}", "", "| Status | Cells |", "|---|---|"]
33
+ lines += [f"| {status} | {counts[status]} |" for status in ("PASS", "FAIL", "UNRESOLVED")]
34
+ if computed:
35
+ lines += ["", f"{len(computed)} formula cells had no saved value. Their expected values were computed "
36
+ "by the `formulas` library, not by Excel, and are marked (computed)."]
37
+ if notes:
38
+ lines += ["", "## Regions", ""] + [f"- {n}" for n in notes]
39
+ if findings:
40
+ lines += ["", "## Findings in the workbook", ""] + [f"- {f}" for f in findings]
41
+ failed = [v for v in verdicts if v.status == "FAIL"]
42
+ if failed:
43
+ lines += ["", "## Failed cells", "", "| Cell | Excel | SQL |", "|---|---|---|"]
44
+ lines += [f"| {a1(v.key)} | {_md(v.expected)}{' (computed)' if v.key in computed else ''} | {_md(v.actual)} |"
45
+ for v in failed]
46
+ unresolved = [v for v in verdicts if v.status == "UNRESOLVED"]
47
+ if unresolved:
48
+ lines += ["", "## Unresolved cells", "", "| Cell | Reason |", "|---|---|"]
49
+ lines += [f"| {a1(v.key)} | {_md(v.reason)} |" for v in unresolved]
50
+ (out / "REPORT.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
51
+ return counts
cellproof/verifier.py ADDED
@@ -0,0 +1,62 @@
1
+ """Run the compiled batches on DuckDB and compare every cell with Excel's saved value."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ import duckdb
9
+
10
+ from cellproof.compiler import READ_SEED, Compiled, batch_rows, write_seed
11
+ from cellproof.model import Book, Cell, Key, Verdict, a1
12
+
13
+
14
+ class SqlFailed(Exception):
15
+ """DuckDB rejected SQL that cellproof generated (a bug: every supported formula must run)."""
16
+
17
+
18
+ def execute(book: Book, compiled: Compiled) -> dict[Key, tuple]:
19
+ """The same row SQL as the dbt project: the seed, then each batch appended in place. Returns formula cells only."""
20
+ con = duckdb.connect()
21
+ with tempfile.TemporaryDirectory() as tmp:
22
+ seed = Path(tmp) / "input_cells.csv"
23
+ write_seed(book, seed)
24
+ con.execute(f"CREATE TABLE cells AS SELECT * FROM {READ_SEED}", [str(seed)])
25
+ for batch in compiled.batches:
26
+ try:
27
+ con.execute(f"INSERT INTO cells {batch_rows(batch, 'cells')}")
28
+ except duckdb.Error as err:
29
+ # ponytail: the run stops; marking just this batch's cells (and their dependents) UNRESOLVED needs a recompile
30
+ raise SqlFailed(f"DuckDB could not run the SQL for {a1(batch[0].key)} and {len(batch) - 1} cells after it "
31
+ f"({str(err).splitlines()[0]}). This is a bug in cellproof; please report it.") from err
32
+ wanted = {s.key for s in compiled.steps}
33
+ rows = con.execute("SELECT * FROM cells").fetchall()
34
+ return {(s, r, c): (k, n, t) for s, r, c, k, n, t in rows if (s, r, c) in wanted}
35
+
36
+
37
+ def matches(cell: Cell, kind: str, num, txt) -> bool:
38
+ """Excel's saved value vs the SQL result. An Excel error matches only the same error code."""
39
+ if cell.kind == "e":
40
+ return kind == "e" and txt == cell.value
41
+ if kind == "e" or (num is None and txt is None):
42
+ return False
43
+ if cell.kind == "n":
44
+ return kind == "n" and math.isclose(cell.value, num, rel_tol=1e-9, abs_tol=1e-12)
45
+ if cell.kind == "b":
46
+ return kind == "b" and num == (1.0 if cell.value else 0.0)
47
+ if cell.kind == "s":
48
+ return kind == "s" and txt == cell.value
49
+ return False
50
+
51
+
52
+ def verify(book: Book, compiled: Compiled) -> list[Verdict]:
53
+ got = execute(book, compiled)
54
+ out = []
55
+ for step in compiled.steps:
56
+ cell = book.cells[step.key]
57
+ kind, n, t = got[step.key]
58
+ actual = t if kind in ("s", "e") else (bool(n) if kind == "b" and n is not None else n)
59
+ out.append(Verdict(step.key, "PASS" if matches(cell, kind, n, t) else "FAIL", cell.value, actual))
60
+ for key, reason in compiled.unresolved.items():
61
+ out.append(Verdict(key, "UNRESOLVED", book.cells[key].value, None, reason))
62
+ return sorted(out, key=lambda v: v.key)
@@ -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)
@@ -0,0 +1,33 @@
1
+ cellproof/__init__.py,sha256=qbhMh4mg2JMoCFflKwIRvApU63RR8AxvYq1OT__8sgI,27
2
+ cellproof/__main__.py,sha256=VwT4B2Uhc4sjZoXMJicnl8mM0ND-lRucvdeeZKcpaWs,57
3
+ cellproof/cli.py,sha256=Atc4jxDcP4ESybIaBttxjcFh3hqSL39qRuQV9mUS0Bg,2444
4
+ cellproof/compiler.py,sha256=wGinNTP5dorROaEJMWzRZgz43a-z4K5j2j3WS1gkAxs,19519
5
+ cellproof/compute.py,sha256=ONf-3wV9ZsxRFoTAEHrgsCdp7m4hRDssfWaYCOs47XU,2685
6
+ cellproof/emitter.py,sha256=MQJea7LEwqEzFKrIG2qcHXXEZngm1kQuQIDnETyuRes,6344
7
+ cellproof/expr.py,sha256=iexLxvwRnB4oT1HP2XiANQv8MYlZ6Lw-10AUM6DPeqk,8993
8
+ cellproof/fenwick.py,sha256=N9-uMNlykMMhzJfkDEtNxsYeXFscR2BHYl6Vw-lHz4Q,1348
9
+ cellproof/functions.py,sha256=XCejVodGJp290w_w2rW9oEpyjBBHMY2eXMJYlxb1MX8,4206
10
+ cellproof/graph.py,sha256=rjFVe00e8iyK5Rd5ySoHD2SAqHCw4hVlPEoE_oL2SMY,8271
11
+ cellproof/model.py,sha256=lox07H1v9bCKovagRbs7GhDMf5PtmOwpRh785ajJDiI,2423
12
+ cellproof/parser.py,sha256=PL4qb-pNQwtNEw9rOj9g1bDTrvzOIb9aGzds5mHpbTA,4946
13
+ cellproof/r1c1.py,sha256=xY0INUlfzjucM7A6LCqKo65fPQt0Z_kBezmVWT1yeWk,8436
14
+ cellproof/rangeindex.py,sha256=qxL5D3AnULfohTHeLjqJTaxhgl4aoMUBPwRM4sCXpeU,4496
15
+ cellproof/reader.py,sha256=mazmjCwq3QqRm9Wmwbr7u_o5A4vwC_0N35yGJ-z1PJE,3542
16
+ cellproof/refs.py,sha256=q4j46GxhEDg97GzmhghMqnrDKOYwhgLTwrPIPsRk0V0,1807
17
+ cellproof/region.py,sha256=OhLCxD4YEZpWeAXG9Ha4dOhGLZ2-fleMROYvYMk7a5U,19151
18
+ cellproof/regions.py,sha256=6QYZsJaS1eyFX7FN_RgrUd_D4YYvD0695UWOgDnablE,11888
19
+ cellproof/report.py,sha256=5uUuBENrKJI5GULMDQ4uN1Mx5NmCzr8GQBfv2NAgQHc,2365
20
+ cellproof/verifier.py,sha256=LGzNsJF2BUhifjZ8bfQazBd7-nL8K3AJipEkFAYJZEY,2780
21
+ cellproof/fn/__init__.py,sha256=FCehhK5QagM3aJJhMo92lTSj0uS_Z3FSbMZ57Ht_l9w,206
22
+ cellproof/fn/aggregate.py,sha256=aKDPArE9rJhMnL9Cpvic6vZLK1AykR3uhCtWT7g9Ars,8040
23
+ cellproof/fn/criteria.py,sha256=_A9ZbGnu8loKwYwZscwzffn5pQ3xGSnABLij_jBrIMk,9648
24
+ cellproof/fn/dates.py,sha256=9SN-v143i1uFcR9V8Xa55KLQ2oyOGM1sqpS6MDY_IFU,8066
25
+ cellproof/fn/logic.py,sha256=gQ4qAQsFxNAHA1yZRInaZw8Mjpv9kPeRdzLY50uG_n8,4742
26
+ cellproof/fn/lookup.py,sha256=AFFJeKS6PahhW-UObvrZYVGgW_H1DyycBXciVpbRI24,14782
27
+ cellproof/fn/maths.py,sha256=dZxxdo4EaTF6Tn7Wb651sWjxYtdOUBBWaGUDJ7QQPWw,4992
28
+ cellproof/fn/text.py,sha256=xiw6O7HkEe-Ov3umtUijaEPG-0ogpyAhD31N16B3u5U,10872
29
+ cellproof-0.2.0.dist-info/METADATA,sha256=olWEUvIiBD3MUsjCx9AvSt3JVThtFo9hpy752wWl-XU,9579
30
+ cellproof-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
31
+ cellproof-0.2.0.dist-info/entry_points.txt,sha256=Fmzx5Byo9wZQWQrzM557_61ppO4wRmXrYfwAhwmsD3Y,49
32
+ cellproof-0.2.0.dist-info/licenses/LICENSE,sha256=DwkdN_2ue-4IM8pDfuB5nJ387buNEFyKDadbaB-gu8E,1093
33
+ cellproof-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cellproof = cellproof.cli:main
@@ -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.