sheet-lint 0.1.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.
- sheet_lint-0.1.0/LICENSE +21 -0
- sheet_lint-0.1.0/PKG-INFO +471 -0
- sheet_lint-0.1.0/README.md +439 -0
- sheet_lint-0.1.0/pyproject.toml +67 -0
- sheet_lint-0.1.0/setup.cfg +4 -0
- sheet_lint-0.1.0/src/sheet_lint/__init__.py +38 -0
- sheet_lint-0.1.0/src/sheet_lint/__main__.py +10 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/__init__.py +163 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/duplicates.py +303 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/formulas.py +290 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/headers.py +374 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/keys.py +425 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/rows.py +237 -0
- sheet_lint-0.1.0/src/sheet_lint/checks/types.py +151 -0
- sheet_lint-0.1.0/src/sheet_lint/cli.py +170 -0
- sheet_lint-0.1.0/src/sheet_lint/context.py +104 -0
- sheet_lint-0.1.0/src/sheet_lint/engine.py +177 -0
- sheet_lint-0.1.0/src/sheet_lint/errors.py +14 -0
- sheet_lint-0.1.0/src/sheet_lint/infer.py +223 -0
- sheet_lint-0.1.0/src/sheet_lint/loaders/__init__.py +47 -0
- sheet_lint-0.1.0/src/sheet_lint/loaders/csv_loader.py +127 -0
- sheet_lint-0.1.0/src/sheet_lint/loaders/xlsx_loader.py +482 -0
- sheet_lint-0.1.0/src/sheet_lint/model.py +367 -0
- sheet_lint-0.1.0/src/sheet_lint/profile.py +461 -0
- sheet_lint-0.1.0/src/sheet_lint/py.typed +0 -0
- sheet_lint-0.1.0/src/sheet_lint/report.py +191 -0
- sheet_lint-0.1.0/src/sheet_lint/textutil.py +30 -0
- sheet_lint-0.1.0/src/sheet_lint.egg-info/PKG-INFO +471 -0
- sheet_lint-0.1.0/src/sheet_lint.egg-info/SOURCES.txt +40 -0
- sheet_lint-0.1.0/src/sheet_lint.egg-info/dependency_links.txt +1 -0
- sheet_lint-0.1.0/src/sheet_lint.egg-info/entry_points.txt +2 -0
- sheet_lint-0.1.0/src/sheet_lint.egg-info/requires.txt +4 -0
- sheet_lint-0.1.0/src/sheet_lint.egg-info/top_level.txt +1 -0
- sheet_lint-0.1.0/tests/test_checks.py +296 -0
- sheet_lint-0.1.0/tests/test_cli.py +142 -0
- sheet_lint-0.1.0/tests/test_docs.py +51 -0
- sheet_lint-0.1.0/tests/test_fixtures.py +107 -0
- sheet_lint-0.1.0/tests/test_infer.py +104 -0
- sheet_lint-0.1.0/tests/test_loaders.py +234 -0
- sheet_lint-0.1.0/tests/test_model.py +220 -0
- sheet_lint-0.1.0/tests/test_profile.py +144 -0
- sheet_lint-0.1.0/tests/test_report.py +143 -0
sheet_lint-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sheet-lint 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.
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sheet-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit one CSV or Excel workbook for the quiet defects that make a spreadsheet return a slightly wrong number.
|
|
5
|
+
Author: devops-gm88
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/devops-gm88/sheet-lint
|
|
8
|
+
Project-URL: Source, https://github.com/devops-gm88/sheet-lint
|
|
9
|
+
Keywords: spreadsheet,csv,xlsx,excel,audit,data-quality,data-validation,cli,standard-library
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# sheet-lint
|
|
34
|
+
|
|
35
|
+
[](https://github.com/devops-gm88/sheet-lint/actions/workflows/ci.yml)
|
|
36
|
+
[](pyproject.toml)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
Point it at a CSV or an Excel workbook and it tells you, in ranked order, what
|
|
40
|
+
is quietly wrong with it - with the sheet, column and row behind every claim.
|
|
41
|
+
|
|
42
|
+
Python 3.9+ - **standard library only, zero dependencies** - and no network
|
|
43
|
+
access at any point.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 -m pip install git+https://github.com/devops-gm88/sheet-lint.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Not on PyPI yet - the GitHub URL above is the install. It is one console script
|
|
50
|
+
with no dependencies behind it.
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
$ sheet-lint invoices.xlsx
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Why this exists
|
|
57
|
+
|
|
58
|
+
Most spreadsheet problems announce themselves. A deleted tab breaks a formula
|
|
59
|
+
with `#REF!`. A bad paste produces `#VALUE!`. A missing file fails loudly.
|
|
60
|
+
|
|
61
|
+
The expensive ones do not. They are the ones where a number comes out, looks
|
|
62
|
+
plausible, and is wrong:
|
|
63
|
+
|
|
64
|
+
- a `SUM` range that was sized before somebody appended thirty rows, so the
|
|
65
|
+
total is short by exactly those rows and nothing on screen says so;
|
|
66
|
+
- a column of amounts where a few values were pasted in as text, so `SUM`
|
|
67
|
+
skips them while `COUNT`, `AVERAGE` and the sort order each disagree with
|
|
68
|
+
the others;
|
|
69
|
+
- one invoice key typed twice, so that customer's spend is counted twice in
|
|
70
|
+
one report and once in another;
|
|
71
|
+
- two blank rows in the middle of a table, so everything below them fell
|
|
72
|
+
outside the filter, the pivot range and the last paste-down;
|
|
73
|
+
- a column headed `Amount` that holds something else entirely;
|
|
74
|
+
- an identifier carrying a trailing space, so an exact-match lookup silently
|
|
75
|
+
finds nothing and returns a confident blank.
|
|
76
|
+
|
|
77
|
+
None of these raise an error. They produce a number that somebody takes to a
|
|
78
|
+
meeting.
|
|
79
|
+
|
|
80
|
+
sheet-lint reads a file the way a careful auditor would - judging each cell
|
|
81
|
+
against the whole column it sits in - and reports what it found, worst first,
|
|
82
|
+
with a locator for every finding. It is a static analysis pass. It never
|
|
83
|
+
modifies your file.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
sheet-lint is not published to PyPI. Install it from a clone:
|
|
88
|
+
|
|
89
|
+
```console
|
|
90
|
+
git clone https://github.com/devops-gm88/sheet-lint.git
|
|
91
|
+
cd sheet-lint
|
|
92
|
+
python3 -m pip install -e .
|
|
93
|
+
sheet-lint path/to/your-workbook.xlsx
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That installs a `sheet-lint` console script. Nothing else gets installed: the
|
|
97
|
+
project declares no dependencies.
|
|
98
|
+
|
|
99
|
+
To try it without installing anything at all, run it straight from the source
|
|
100
|
+
tree:
|
|
101
|
+
|
|
102
|
+
```console
|
|
103
|
+
$ PYTHONPATH=src python3 -m sheet_lint path/to/your-workbook.xlsx
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or run it on the fixtures that ship with the repository:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
$ sheet-lint tests/fixtures/messy.xlsx
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## What it looks like
|
|
113
|
+
|
|
114
|
+
Real output, from the committed fixture `tests/fixtures/duplicates.csv`
|
|
115
|
+
(19 records, two repeated invoice keys and one customer entered twice):
|
|
116
|
+
|
|
117
|
+
```console
|
|
118
|
+
sheet-lint 0.1.0
|
|
119
|
+
file tests/fixtures/duplicates.csv (csv, 1 sheet)
|
|
120
|
+
sheet duplicates columns A-G | 19 populated row(s) | identifier: 'Invoice ID' (A)
|
|
121
|
+
|
|
122
|
+
2 findings (1 high, 1 medium)
|
|
123
|
+
|
|
124
|
+
[1] HIGH duplicate_key
|
|
125
|
+
19 populated Invoice ID values collapse to 17 distinct identifiers; 2 rows repeat a key that is already in use
|
|
126
|
+
why Every total, join and export that reads this column counts the repeated key twice, so one record quietly becomes two.
|
|
127
|
+
where
|
|
128
|
+
duplicates!A8 key 'INV-1004' repeats the value at A5
|
|
129
|
+
duplicates!A15 key 'INV-1011' repeats the value at A13
|
|
130
|
+
|
|
131
|
+
[2] MEDIUM near_duplicate_records
|
|
132
|
+
1 row pair matches at 90% or better outside the identifier column (case, spacing and punctuation ignored)
|
|
133
|
+
why Two spellings of one record are two records to every total and every join, so one customer's spend is split across two rows.
|
|
134
|
+
where
|
|
135
|
+
duplicates!A7:G7 96% identical to row 17 outside the identifier column
|
|
136
|
+
|
|
137
|
+
notes
|
|
138
|
+
- 4 checks apply only to .xlsx and did not run on this file: hidden_rows, range_stops_short, hardcoded_in_formula_column, literal_constant_in_formulas
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Exit code `1`, because findings exist.
|
|
142
|
+
|
|
143
|
+
Here is the workbook fixture, `tests/fixtures/messy.xlsx`, trimmed after the
|
|
144
|
+
first four findings:
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
sheet-lint 0.1.0
|
|
148
|
+
file tests/fixtures/messy.xlsx (xlsx, 2 sheets)
|
|
149
|
+
sheet Orders columns A-G | 16 populated row(s) | identifier: 'Invoice ID' (A)
|
|
150
|
+
sheet Rates columns A-C | 5 populated row(s) | identifier: 'Label' (A)
|
|
151
|
+
|
|
152
|
+
9 findings (4 high, 4 medium, 1 low)
|
|
153
|
+
|
|
154
|
+
[1] HIGH blank_key_values
|
|
155
|
+
1 of 15 records has an empty Invoice ID
|
|
156
|
+
why A record with no key cannot be looked up or joined, and an export or pivot that keys on this column drops it without saying so.
|
|
157
|
+
where
|
|
158
|
+
Orders!A9 row has data but no Invoice ID
|
|
159
|
+
|
|
160
|
+
[2] HIGH duplicate_key
|
|
161
|
+
15 populated Invoice ID values collapse to 14 distinct identifiers; 1 row repeats a key that is already in use
|
|
162
|
+
why Every total, join and export that reads this column counts the repeated key twice, so one record quietly becomes two.
|
|
163
|
+
where
|
|
164
|
+
Orders!A11 key 'INV-1004' repeats the value at A5
|
|
165
|
+
|
|
166
|
+
[3] HIGH range_stops_short
|
|
167
|
+
1 formula range ends above rows that hold values, so those rows are outside the calculation
|
|
168
|
+
why This is the quiet one: the formula is valid, the result is a number, and it is simply missing the rows below the range, so the sheet understates a total with nothing to indicate it.
|
|
169
|
+
where
|
|
170
|
+
Orders!F14 = SUM(F2:F12) covers F2:F12, but column F holds data through row 17; first value outside the range is F13
|
|
171
|
+
|
|
172
|
+
... (four more findings follow, including:)
|
|
173
|
+
|
|
174
|
+
[7] MEDIUM hardcoded_in_formula_column
|
|
175
|
+
column F (Amount) is built from formulas in 15 of its 16 populated rows, but 1 cell holds a number typed by hand
|
|
176
|
+
why The typed cell keeps whatever it was given: when the inputs around it change it stays put, so one row of the column quietly disagrees with the other 15.
|
|
177
|
+
where
|
|
178
|
+
Orders!F7 column F is calculated in 15 of 16 populated rows, but this cell holds the typed value '612.5'
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
[9] LOW literal_constant_in_formulas
|
|
182
|
+
the value 1.13 is written into 5 formulas in this sheet instead of being held in one cell
|
|
183
|
+
why A rate or threshold typed into formulas cannot be found by searching cells, so changing it means changing every formula and missing one has no visible effect.
|
|
184
|
+
where
|
|
185
|
+
Rates!C2 =B2*1.13
|
|
186
|
+
Rates!C3 =B3*1.13
|
|
187
|
+
Rates!C4 =B4*1.13
|
|
188
|
+
Rates!C5 =B5*1.13
|
|
189
|
+
Rates!C6 =B6*1.13
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A file with nothing wrong with it says so, and exits `0`:
|
|
193
|
+
|
|
194
|
+
```console
|
|
195
|
+
sheet-lint 0.1.0
|
|
196
|
+
file tests/fixtures/clean.csv (csv, 1 sheet)
|
|
197
|
+
sheet clean columns A-G | 40 populated row(s) | identifier: 'Invoice ID' (A)
|
|
198
|
+
|
|
199
|
+
no findings
|
|
200
|
+
|
|
201
|
+
notes
|
|
202
|
+
- 4 checks apply only to .xlsx and did not run on this file: hidden_rows, range_stops_short, hardcoded_in_formula_column, literal_constant_in_formulas
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Exit codes
|
|
206
|
+
|
|
207
|
+
| Code | Meaning |
|
|
208
|
+
| ---- | ------- |
|
|
209
|
+
| `0` | The file was read and nothing was found |
|
|
210
|
+
| `1` | The file was read and at least one finding was reported |
|
|
211
|
+
| `2` | The file could not be read or linted |
|
|
212
|
+
|
|
213
|
+
`--fail-on {high,medium,low}` sets the lowest severity that produces exit code
|
|
214
|
+
`1`, so a pipeline can ignore cosmetic findings:
|
|
215
|
+
|
|
216
|
+
```console
|
|
217
|
+
$ sheet-lint --fail-on high invoices.xlsx
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Options
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
usage: sheet-lint [-h] [--header-row N] [--json] [--max-examples N]
|
|
224
|
+
[--fail-on {high,medium,low}] [--color {auto,always,never}]
|
|
225
|
+
[--quiet] [--list-checks] [--version]
|
|
226
|
+
[path]
|
|
227
|
+
|
|
228
|
+
Audit one CSV or Excel workbook for the defects that make a spreadsheet return
|
|
229
|
+
a slightly wrong number: duplicate and near-duplicate records, unusable key
|
|
230
|
+
columns, numbers stored as text, rows that are silently excluded, formula
|
|
231
|
+
ranges that stop short, and headers that do not match their column.
|
|
232
|
+
|
|
233
|
+
positional arguments:
|
|
234
|
+
path the .csv, .tsv, .txt, .xlsx or .xlsm file to audit
|
|
235
|
+
(required unless --list-checks is used)
|
|
236
|
+
|
|
237
|
+
options:
|
|
238
|
+
-h, --help show this help message and exit
|
|
239
|
+
--header-row N 1-based row holding the column headers (default: 1)
|
|
240
|
+
--json write the report as JSON instead of text
|
|
241
|
+
--max-examples N how many locations to list per finding (default: 8)
|
|
242
|
+
--fail-on {high,medium,low}
|
|
243
|
+
lowest severity that produces exit code 1 (default:
|
|
244
|
+
low)
|
|
245
|
+
--color {auto,always,never}
|
|
246
|
+
colour the severity column (default: auto)
|
|
247
|
+
--quiet print nothing; use the exit code only
|
|
248
|
+
--list-checks list every check and the formats it runs on, then exit
|
|
249
|
+
--version show program's version number and exit
|
|
250
|
+
|
|
251
|
+
exit codes: 0 clean, 1 findings, 2 error. Every finding is reported with the
|
|
252
|
+
sheet, column and row or range it came from.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| Flag | What it does |
|
|
256
|
+
| ---- | ------------ |
|
|
257
|
+
| `--header-row N` | Use row `N` as the header row (default `1`). The report says when a title row above the header looks likely. |
|
|
258
|
+
| `--json` | Emit the same report as JSON, for a script or a findings spreadsheet. |
|
|
259
|
+
| `--max-examples N` | How many locations to list per finding (default `8`). The true count is always reported. |
|
|
260
|
+
| `--fail-on` | Lowest severity producing exit code `1` (default `low`). |
|
|
261
|
+
| `--color` | `auto` (default, honours `NO_COLOR`), `always`, `never`. JSON is never coloured. |
|
|
262
|
+
| `--quiet` | Print nothing; use the exit code only. |
|
|
263
|
+
| `--list-checks` | List every check and the formats it runs on. |
|
|
264
|
+
| `--version` | Print the version. |
|
|
265
|
+
|
|
266
|
+
### JSON
|
|
267
|
+
|
|
268
|
+
`--json` writes one object carrying the same information as the text report,
|
|
269
|
+
including every locator:
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"tool": "sheet-lint",
|
|
274
|
+
"version": "0.1.0",
|
|
275
|
+
"file": "tests/fixtures/blank_key.csv",
|
|
276
|
+
"format": "csv",
|
|
277
|
+
"sheets": [
|
|
278
|
+
{
|
|
279
|
+
"name": "blank_key",
|
|
280
|
+
"kind": "csv",
|
|
281
|
+
"columns": 7,
|
|
282
|
+
"data_rows": 15,
|
|
283
|
+
"records": 15,
|
|
284
|
+
"first_data_row": 2,
|
|
285
|
+
"last_data_row": 16,
|
|
286
|
+
"identifier_column": {
|
|
287
|
+
"letter": "A",
|
|
288
|
+
"header": "Invoice ID",
|
|
289
|
+
"reason": "column A header \"Invoice ID\" names an identifier",
|
|
290
|
+
"distinct_values": 12
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"summary": {
|
|
295
|
+
"findings": 1,
|
|
296
|
+
"high": 1,
|
|
297
|
+
"medium": 0,
|
|
298
|
+
...
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Use it as a library
|
|
303
|
+
|
|
304
|
+
The CLI is a thin wrapper over the same code:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from sheet_lint import lint_file
|
|
308
|
+
|
|
309
|
+
result = lint_file("book.xlsx", header_row=1)
|
|
310
|
+
|
|
311
|
+
print(result.clean, len(result.findings), result.severity_counts())
|
|
312
|
+
for finding in result.findings:
|
|
313
|
+
print(finding.severity.value.upper(), finding.code, "-", finding.summary)
|
|
314
|
+
for location in finding.locations:
|
|
315
|
+
print(" ", location.render()) # e.g. Orders!F14 = SUM(F2:F12) ...
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## What it checks
|
|
319
|
+
|
|
320
|
+
21 checks, as listed by `sheet-lint --list-checks`:
|
|
321
|
+
|
|
322
|
+
```console
|
|
323
|
+
sheet-lint 0.1.0 runs 21 checks:
|
|
324
|
+
duplicate_key csv, xlsx Duplicate identifier values
|
|
325
|
+
duplicate_rows csv, xlsx Whole rows repeated
|
|
326
|
+
near_duplicate_records csv, xlsx Near-duplicate records
|
|
327
|
+
missing_key_column csv, xlsx No identifier column
|
|
328
|
+
blank_key_values csv, xlsx Records with no identifier
|
|
329
|
+
padded_key_values csv, xlsx Identifier values with stray whitespace
|
|
330
|
+
inconsistent_key_format csv, xlsx Identifier column with mixed conventions
|
|
331
|
+
key_sequence_gap csv, xlsx Gaps in the identifier sequence
|
|
332
|
+
numbers_stored_as_text csv, xlsx Numbers held as text
|
|
333
|
+
mixed_types_in_column csv, xlsx Mixed types in one column
|
|
334
|
+
rows_after_gap csv, xlsx Blank rows with data underneath
|
|
335
|
+
total_row_not_last csv, xlsx A total row is not the last row
|
|
336
|
+
hidden_rows xlsx Hidden rows or columns
|
|
337
|
+
blank_header csv, xlsx Columns with data and no header
|
|
338
|
+
duplicate_header csv, xlsx Two columns with the same header
|
|
339
|
+
header_row_missing csv, xlsx No header row
|
|
340
|
+
header_content_mismatch csv, xlsx Header does not match the column
|
|
341
|
+
possible_title_row csv, xlsx A title row above the header row
|
|
342
|
+
range_stops_short xlsx A formula range stops before the data ends
|
|
343
|
+
hardcoded_in_formula_column xlsx A typed value inside a calculated column
|
|
344
|
+
literal_constant_in_formulas xlsx A constant typed into several formulas
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
A CSV run is honest about what it skipped: a CSV has no formulas and no hidden
|
|
348
|
+
rows, so the `.xlsx`-only checks do not run, and the report says so in its
|
|
349
|
+
`notes` section instead of implying a clean bill of health.
|
|
350
|
+
|
|
351
|
+
### How the checks decide
|
|
352
|
+
|
|
353
|
+
- **Records are counted on an identifier, not on rows.** sheet-lint picks one
|
|
354
|
+
identifier column per sheet - by header (`Invoice ID`, `Order Number`,
|
|
355
|
+
`SKU`, `Ref`) or, failing that, the leftmost column whose values are all
|
|
356
|
+
distinct. Findings quote figures like "19 populated Invoice ID values
|
|
357
|
+
collapse to 17 distinct identifiers". Identity ignores case, spacing,
|
|
358
|
+
punctuation and leading zeros, so `007` and `7` are one identifier written
|
|
359
|
+
two ways.
|
|
360
|
+
- **A sheet with no usable identifier is reported as such**
|
|
361
|
+
(`missing_key_column`) rather than keyed on an arbitrary column.
|
|
362
|
+
- **Header checks use a vocabulary of header words.** `Amount`, `Total`,
|
|
363
|
+
`Qty`, `Date`, `Email`, `Invoice ID` and similar words carry an expectation;
|
|
364
|
+
a header sheet-lint does not recognise is left alone instead of guessed at.
|
|
365
|
+
- **Near-duplicate records** are compared on everything except the identifier
|
|
366
|
+
column, at 90% text similarity or better, after blocking rows by vocabulary
|
|
367
|
+
overlap and length so that the comparison stays affordable.
|
|
368
|
+
- **Every finding carries a locator** - a cell such as `Orders!F14` or a range
|
|
369
|
+
such as `Orders!A14:G17`. The `Finding` type refuses to be built without
|
|
370
|
+
one, so an unlocatable claim cannot reach the report.
|
|
371
|
+
- **Excel dates are read through the cell format.** A workbook stores a date
|
|
372
|
+
as a serial number plus a date number format, so `45659` is recognised as a
|
|
373
|
+
date because the cell is formatted as one - which is why `dates.xlsx`, a
|
|
374
|
+
clean workbook of real dates, produces no findings.
|
|
375
|
+
- **Nothing is recalculated.** Formulas are read as stored, cached results
|
|
376
|
+
included.
|
|
377
|
+
|
|
378
|
+
## What it does not do
|
|
379
|
+
|
|
380
|
+
- **It never changes your file.** It only reads.
|
|
381
|
+
- **It does not recalculate formulas.** Cached values are used as stored, so a
|
|
382
|
+
stale cached value is linted exactly as the file holds it.
|
|
383
|
+
- **It does not know your business rules.** It cannot tell that a formula
|
|
384
|
+
multiplies the wrong two columns, that a price is out of date, or that a
|
|
385
|
+
total is impossible for your business. It finds structural defects, not
|
|
386
|
+
wrong intentions.
|
|
387
|
+
- **It does not read every spreadsheet format.** `.csv`, `.tsv`, `.txt`,
|
|
388
|
+
`.xlsx` and `.xlsm` only - not the legacy binary `.xls`, not `.ods`, not a
|
|
389
|
+
Google Sheets URL, not a password-protected workbook.
|
|
390
|
+
- **`.xlsx` support is a defined subset**: cell values, formulas, shared
|
|
391
|
+
strings, hidden rows and columns, and number formats (used to recognise real
|
|
392
|
+
dates and Text-formatted cells). Charts, pivot tables,
|
|
393
|
+
defined names, conditional formatting and data validation are outside it and
|
|
394
|
+
are skipped without complaint. It never fakes a part it did not read.
|
|
395
|
+
- **CSV has no types, so type checks work by inference.** A CSV cell is text;
|
|
396
|
+
sheet-lint infers what it looks like and reports what it observed.
|
|
397
|
+
- **The near-duplicate scan has a finite pair budget.** On a very large sheet
|
|
398
|
+
the report says when that budget ran out instead of quietly returning a
|
|
399
|
+
short answer.
|
|
400
|
+
- **Silence is not certification.** A check that reports nothing found
|
|
401
|
+
nothing; it is not a statement that the column is correct.
|
|
402
|
+
- **It stops rather than samples.** A file with more than 50,000 rows, or more
|
|
403
|
+
than 1,024 populated columns in a sheet, is reported as an error (exit code
|
|
404
|
+
`2`) instead of being analysed in part. A partial audit that looks complete
|
|
405
|
+
is worse than no audit.
|
|
406
|
+
- **No telemetry, no network, no state.** It reads one file and prints a
|
|
407
|
+
report.
|
|
408
|
+
|
|
409
|
+
## Tests
|
|
410
|
+
|
|
411
|
+
The suite uses `unittest` from the standard library and needs no runner:
|
|
412
|
+
|
|
413
|
+
```console
|
|
414
|
+
$ PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
= 152 tests, all passing on Python 3.9 and Python 3.14. After
|
|
418
|
+
`pip install -e .` the same command runs without `PYTHONPATH`.
|
|
419
|
+
|
|
420
|
+
The fixtures in `tests/fixtures/` are the suite's evidence, and each one is a
|
|
421
|
+
small deliberate case:
|
|
422
|
+
|
|
423
|
+
| Fixture | What it demonstrates |
|
|
424
|
+
| ------- | -------------------- |
|
|
425
|
+
| `clean.csv` | 40 tidy records: the tool must find nothing at all |
|
|
426
|
+
| `duplicates.csv` | Two repeated keys and one customer entered twice, one word apart |
|
|
427
|
+
| `blank_key.csv` | Three records with no identifier, on non-sequential account numbers |
|
|
428
|
+
| `trailing_rows.csv` | Two blank rows with six records underneath them |
|
|
429
|
+
| `key_gaps.csv` | An unbroken invoice sequence with three numbers missing |
|
|
430
|
+
| `key_format.csv` | Identifiers with stray whitespace, and two separator conventions |
|
|
431
|
+
| `messy.xlsx` | A workbook with a blank key, a repeated key, numbers stored as text, a typed-over formula cell, a `SUM` that stops five rows short, a hidden row, a misleading header, and a hardcoded rate |
|
|
432
|
+
| `dates.xlsx` | Real Excel dates - serial numbers wearing a date format - in a workbook that must be reported clean |
|
|
433
|
+
|
|
434
|
+
Every fixture is invented. No real workbook contributed to this repository,
|
|
435
|
+
and no organisation or person is named anywhere in it.
|
|
436
|
+
|
|
437
|
+
### Regenerating the fixtures
|
|
438
|
+
|
|
439
|
+
The `.xlsx` fixture is a real workbook written by a small script that builds
|
|
440
|
+
the OOXML parts with `zipfile` and `xml.etree` - no spreadsheet library
|
|
441
|
+
involved, because the project has no dependencies:
|
|
442
|
+
|
|
443
|
+
```console
|
|
444
|
+
$ python3 tools/make_fixtures.py
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
The output is deterministic, and a test asserts that the committed fixtures
|
|
448
|
+
match what the generator produces byte for byte.
|
|
449
|
+
|
|
450
|
+
## Layout
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
src/sheet_lint/
|
|
454
|
+
cli.py argument parsing, exit codes, stream handling
|
|
455
|
+
engine.py load, run every applicable check, rank the findings
|
|
456
|
+
model.py Cell, Sheet, Location, Finding, Severity
|
|
457
|
+
infer.py what a value looks like it is (number, date, email, text)
|
|
458
|
+
profile.py per-column measurements, identifier column selection
|
|
459
|
+
context.py the facts handed to each check
|
|
460
|
+
loaders/ csv_loader.py, xlsx_loader.py (zipfile + xml.etree)
|
|
461
|
+
checks/ one module per family of defects
|
|
462
|
+
report.py ranked plain text, or JSON
|
|
463
|
+
tools/
|
|
464
|
+
make_fixtures.py regenerates tests/fixtures
|
|
465
|
+
tests/
|
|
466
|
+
test_*.py unittest suite, one module per area
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## License
|
|
470
|
+
|
|
471
|
+
MIT. See `LICENSE`.
|