sheet-unmess 1.0.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_unmess-1.0.0/.gitignore +51 -0
- sheet_unmess-1.0.0/PKG-INFO +139 -0
- sheet_unmess-1.0.0/README.md +111 -0
- sheet_unmess-1.0.0/pyproject.toml +46 -0
- sheet_unmess-1.0.0/sheet_unmess/__init__.py +16 -0
- sheet_unmess-1.0.0/sheet_unmess/ai_rules.py +79 -0
- sheet_unmess-1.0.0/sheet_unmess/classifier.py +94 -0
- sheet_unmess-1.0.0/sheet_unmess/cli.py +98 -0
- sheet_unmess-1.0.0/sheet_unmess/core.py +180 -0
- sheet_unmess-1.0.0/sheet_unmess/detector.py +164 -0
- sheet_unmess-1.0.0/sheet_unmess/mcp_server.py +154 -0
- sheet_unmess-1.0.0/sheet_unmess/normalizer.py +125 -0
- sheet_unmess-1.0.0/sheet_unmess/receipt.py +121 -0
- sheet_unmess-1.0.0/sheet_unmess/reconciler.py +59 -0
- sheet_unmess-1.0.0/tests/__init__.py +0 -0
- sheet_unmess-1.0.0/tests/test_sheet_unmess.py +113 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
# Python
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Testing
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Build artifacts
|
|
19
|
+
build/
|
|
20
|
+
dist/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.eggs/
|
|
23
|
+
|
|
24
|
+
# IDE and local AI configuration
|
|
25
|
+
.vscode/
|
|
26
|
+
.cursor/
|
|
27
|
+
.cursorrules
|
|
28
|
+
.windsurfrules
|
|
29
|
+
|
|
30
|
+
# OS files
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
33
|
+
desktop.ini
|
|
34
|
+
|
|
35
|
+
# Local test outputs
|
|
36
|
+
sample.csv
|
|
37
|
+
cleaned.csv
|
|
38
|
+
audit.md
|
|
39
|
+
merged_test.xlsx
|
|
40
|
+
merged_cleaned.csv
|
|
41
|
+
merged_audit.md
|
|
42
|
+
nested_test.xlsx
|
|
43
|
+
nested_cleaned.csv
|
|
44
|
+
nested_audit.md
|
|
45
|
+
multi_sheet_test.xlsx
|
|
46
|
+
sales_cleaned.csv
|
|
47
|
+
sales_audit.md
|
|
48
|
+
expenses_cleaned.csv
|
|
49
|
+
expenses_audit.md
|
|
50
|
+
employees_cleaned.csv
|
|
51
|
+
employees_audit.md
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sheet-unmess
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Forensic Messy-Excel & Dirty-CSV Ingestor with Tamper-Evident Audit Receipts and Native MCP Server
|
|
5
|
+
Project-URL: Homepage, https://github.com/sheet-unmess/sheet-unmess
|
|
6
|
+
Project-URL: Documentation, https://github.com/sheet-unmess/sheet-unmess#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/sheet-unmess/sheet-unmess
|
|
8
|
+
Author: Sheet Unmess Contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: audit,claude,csv,cursor,data-cleaning,excel,llm-agent,mcp,mcp-server,pandas,windsurf
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
21
|
+
Requires-Dist: pandas>=2.0.0
|
|
22
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: polars>=0.20.0; extra == 'all'
|
|
25
|
+
Provides-Extra: polars
|
|
26
|
+
Requires-Dist: polars>=0.20.0; extra == 'polars'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# sheet-unmess
|
|
30
|
+
|
|
31
|
+
Forensic-style ingestion for messy Excel (`.xlsx`) and CSV files. It attempts to
|
|
32
|
+
identify headers, remove common non-data rows, normalize common number formats,
|
|
33
|
+
and produce a human-readable audit receipt.
|
|
34
|
+
|
|
35
|
+
> **Important:** automatic header/row classification is heuristic. Always inspect
|
|
36
|
+
> the output and reconciliation receipt before using cleaned data for financial,
|
|
37
|
+
> regulatory, or other high-stakes decisions. A receipt is an audit report, not a
|
|
38
|
+
> cryptographic signature or proof that no data was lost.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- Detects likely header rows and combines adjacent stacked headers.
|
|
43
|
+
- Propagates values from the top-left cell across merged Excel ranges.
|
|
44
|
+
- Classifies common title, blank, footnote, subtotal, and grand-total rows.
|
|
45
|
+
- Parses common US/EU number formats, accounting negatives, and percentages.
|
|
46
|
+
- Optionally returns a cleaning report with dropped-row details and total checks.
|
|
47
|
+
- Provides a CLI and a stdio MCP server for compatible AI clients.
|
|
48
|
+
|
|
49
|
+
## Install from source
|
|
50
|
+
|
|
51
|
+
Python 3.10 or newer is required.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m venv .venv
|
|
55
|
+
# Windows PowerShell:
|
|
56
|
+
.venv\Scripts\Activate.ps1
|
|
57
|
+
# macOS/Linux:
|
|
58
|
+
source .venv/bin/activate
|
|
59
|
+
|
|
60
|
+
python -m pip install --upgrade pip
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from sheet_unmess import read_messy
|
|
68
|
+
|
|
69
|
+
df = read_messy("quarterly_report.xlsx")
|
|
70
|
+
print(df.head())
|
|
71
|
+
|
|
72
|
+
report = read_messy("quarterly_report.xlsx", return_report=True)
|
|
73
|
+
print(report.data.head())
|
|
74
|
+
print(report.receipt.to_markdown())
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For European day-first dates, pass `date_dayfirst=True`.
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
sheet-unmess clean input.xlsx --out cleaned.csv --receipt audit.md
|
|
83
|
+
sheet-unmess clean input.csv --out cleaned.csv --dayfirst
|
|
84
|
+
sheet-unmess clean input.xlsx --sheet Financials --out cleaned.csv
|
|
85
|
+
sheet-unmess init-ai
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`--out` writes CSV by default; use a `.parquet` extension for Parquet output
|
|
89
|
+
(and install a suitable Parquet engine such as `pyarrow`).
|
|
90
|
+
|
|
91
|
+
## Tests
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m pip install -e ".[dev]"
|
|
95
|
+
python -m pytest
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or run the included test file directly:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python tests/test_sheet_unmess.py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## MCP server
|
|
105
|
+
|
|
106
|
+
The package exposes `sheet-unmess-mcp` and `sheet-unmess mcp`. Example generic
|
|
107
|
+
stdio MCP configuration:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"sheet-unmess": {
|
|
113
|
+
"command": "python",
|
|
114
|
+
"args": ["-m", "sheet_unmess.mcp_server"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Ensure the `python` executable points to the environment where `sheet-unmess`
|
|
121
|
+
is installed. The `sheet-unmess init-ai` command writes project rules, a Cursor
|
|
122
|
+
MCP configuration, and attempts to update the Claude Desktop configuration.
|
|
123
|
+
Review generated configuration before using it.
|
|
124
|
+
|
|
125
|
+
## Current limitations
|
|
126
|
+
|
|
127
|
+
- `.xls` legacy Excel files are not supported by the current openpyxl loader.
|
|
128
|
+
- Header detection and row classification are heuristic and may need manual review.
|
|
129
|
+
- Reconciliation currently compares embedded totals against aggregated clean-row
|
|
130
|
+
sums; nested subtotals and multiple independent sections can require more
|
|
131
|
+
sophisticated grouping logic.
|
|
132
|
+
- Mixed ambiguous dates cannot be inferred reliably; use `date_dayfirst` when
|
|
133
|
+
appropriate.
|
|
134
|
+
- The MCP server reads files by local path and therefore needs filesystem access
|
|
135
|
+
to the file being processed.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# sheet-unmess
|
|
2
|
+
|
|
3
|
+
Forensic-style ingestion for messy Excel (`.xlsx`) and CSV files. It attempts to
|
|
4
|
+
identify headers, remove common non-data rows, normalize common number formats,
|
|
5
|
+
and produce a human-readable audit receipt.
|
|
6
|
+
|
|
7
|
+
> **Important:** automatic header/row classification is heuristic. Always inspect
|
|
8
|
+
> the output and reconciliation receipt before using cleaned data for financial,
|
|
9
|
+
> regulatory, or other high-stakes decisions. A receipt is an audit report, not a
|
|
10
|
+
> cryptographic signature or proof that no data was lost.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Detects likely header rows and combines adjacent stacked headers.
|
|
15
|
+
- Propagates values from the top-left cell across merged Excel ranges.
|
|
16
|
+
- Classifies common title, blank, footnote, subtotal, and grand-total rows.
|
|
17
|
+
- Parses common US/EU number formats, accounting negatives, and percentages.
|
|
18
|
+
- Optionally returns a cleaning report with dropped-row details and total checks.
|
|
19
|
+
- Provides a CLI and a stdio MCP server for compatible AI clients.
|
|
20
|
+
|
|
21
|
+
## Install from source
|
|
22
|
+
|
|
23
|
+
Python 3.10 or newer is required.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m venv .venv
|
|
27
|
+
# Windows PowerShell:
|
|
28
|
+
.venv\Scripts\Activate.ps1
|
|
29
|
+
# macOS/Linux:
|
|
30
|
+
source .venv/bin/activate
|
|
31
|
+
|
|
32
|
+
python -m pip install --upgrade pip
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from sheet_unmess import read_messy
|
|
40
|
+
|
|
41
|
+
df = read_messy("quarterly_report.xlsx")
|
|
42
|
+
print(df.head())
|
|
43
|
+
|
|
44
|
+
report = read_messy("quarterly_report.xlsx", return_report=True)
|
|
45
|
+
print(report.data.head())
|
|
46
|
+
print(report.receipt.to_markdown())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For European day-first dates, pass `date_dayfirst=True`.
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
sheet-unmess clean input.xlsx --out cleaned.csv --receipt audit.md
|
|
55
|
+
sheet-unmess clean input.csv --out cleaned.csv --dayfirst
|
|
56
|
+
sheet-unmess clean input.xlsx --sheet Financials --out cleaned.csv
|
|
57
|
+
sheet-unmess init-ai
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`--out` writes CSV by default; use a `.parquet` extension for Parquet output
|
|
61
|
+
(and install a suitable Parquet engine such as `pyarrow`).
|
|
62
|
+
|
|
63
|
+
## Tests
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m pip install -e ".[dev]"
|
|
67
|
+
python -m pytest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or run the included test file directly:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python tests/test_sheet_unmess.py
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## MCP server
|
|
77
|
+
|
|
78
|
+
The package exposes `sheet-unmess-mcp` and `sheet-unmess mcp`. Example generic
|
|
79
|
+
stdio MCP configuration:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"sheet-unmess": {
|
|
85
|
+
"command": "python",
|
|
86
|
+
"args": ["-m", "sheet_unmess.mcp_server"]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Ensure the `python` executable points to the environment where `sheet-unmess`
|
|
93
|
+
is installed. The `sheet-unmess init-ai` command writes project rules, a Cursor
|
|
94
|
+
MCP configuration, and attempts to update the Claude Desktop configuration.
|
|
95
|
+
Review generated configuration before using it.
|
|
96
|
+
|
|
97
|
+
## Current limitations
|
|
98
|
+
|
|
99
|
+
- `.xls` legacy Excel files are not supported by the current openpyxl loader.
|
|
100
|
+
- Header detection and row classification are heuristic and may need manual review.
|
|
101
|
+
- Reconciliation currently compares embedded totals against aggregated clean-row
|
|
102
|
+
sums; nested subtotals and multiple independent sections can require more
|
|
103
|
+
sophisticated grouping logic.
|
|
104
|
+
- Mixed ambiguous dates cannot be inferred reliably; use `date_dayfirst` when
|
|
105
|
+
appropriate.
|
|
106
|
+
- The MCP server reads files by local path and therefore needs filesystem access
|
|
107
|
+
to the file being processed.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sheet-unmess"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Forensic Messy-Excel & Dirty-CSV Ingestor with Tamper-Evident Audit Receipts and Native MCP Server"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Sheet Unmess Contributors" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"excel", "csv", "data-cleaning", "pandas", "mcp", "mcp-server",
|
|
17
|
+
"cursor", "windsurf", "claude", "llm-agent", "audit"
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pandas>=2.0.0",
|
|
31
|
+
"openpyxl>=3.1.0",
|
|
32
|
+
"python-dateutil>=2.8.2",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
polars = ["polars>=0.20.0"]
|
|
37
|
+
all = ["polars>=0.20.0"]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
sheet-unmess = "sheet_unmess.cli:main"
|
|
41
|
+
sheet-unmess-mcp = "sheet_unmess.mcp_server:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/sheet-unmess/sheet-unmess"
|
|
45
|
+
Documentation = "https://github.com/sheet-unmess/sheet-unmess#readme"
|
|
46
|
+
Repository = "https://github.com/sheet-unmess/sheet-unmess"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sheet-unmess: Forensic Messy-Excel & Dirty-CSV Ingestor + Audit Receipt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sheet_unmess.core import read_messy, unmess
|
|
6
|
+
from sheet_unmess.receipt import AuditReceipt, CleaningReport
|
|
7
|
+
from sheet_unmess.ai_rules import install_ai_rules
|
|
8
|
+
|
|
9
|
+
__version__ = "1.0.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"read_messy",
|
|
12
|
+
"unmess",
|
|
13
|
+
"AuditReceipt",
|
|
14
|
+
"CleaningReport",
|
|
15
|
+
"install_ai_rules",
|
|
16
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""AI editor rules and MCP configuration installer."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
CURSOR_RULE_CONTENT = """# Spreadsheet ingestion rules
|
|
9
|
+
|
|
10
|
+
When working with Excel (.xlsx) or CSV files, prefer `sheet_unmess.read_messy`
|
|
11
|
+
over raw pandas readers when the file may contain merged/multi-row headers,
|
|
12
|
+
embedded subtotals, mixed locale-formatted numbers, or title/footnote rows.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from sheet_unmess import read_messy
|
|
16
|
+
|
|
17
|
+
report = read_messy("report.xlsx", return_report=True)
|
|
18
|
+
df = report.data
|
|
19
|
+
print(report.receipt.to_markdown())
|
|
20
|
+
```
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def _merge_json(path: Path, key: str, value: dict) -> None:
|
|
24
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
current = {}
|
|
26
|
+
if path.exists():
|
|
27
|
+
try:
|
|
28
|
+
current = json.loads(path.read_text(encoding="utf-8"))
|
|
29
|
+
except (json.JSONDecodeError, OSError):
|
|
30
|
+
current = {}
|
|
31
|
+
current.setdefault(key, {}).update(value)
|
|
32
|
+
path.write_text(json.dumps(current, indent=2), encoding="utf-8")
|
|
33
|
+
|
|
34
|
+
def install_ai_rules(project_root: str = ".") -> dict[str, bool]:
|
|
35
|
+
"""Install project-local rules and Cursor MCP config; preserve other MCP entries."""
|
|
36
|
+
root = Path(project_root).resolve()
|
|
37
|
+
results: dict[str, bool] = {}
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
(root / ".cursorrules").write_text(CURSOR_RULE_CONTENT, encoding="utf-8")
|
|
41
|
+
results[".cursorrules"] = True
|
|
42
|
+
except OSError:
|
|
43
|
+
results[".cursorrules"] = False
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
(root / ".windsurfrules").write_text(CURSOR_RULE_CONTENT, encoding="utf-8")
|
|
47
|
+
results[".windsurfrules"] = True
|
|
48
|
+
except OSError:
|
|
49
|
+
results[".windsurfrules"] = False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
_merge_json(
|
|
53
|
+
root / ".cursor" / "mcp.json",
|
|
54
|
+
"mcpServers",
|
|
55
|
+
{"sheet-unmess": {"command": "python", "args": ["-m", "sheet_unmess.mcp_server"]}},
|
|
56
|
+
)
|
|
57
|
+
results[".cursor/mcp.json"] = True
|
|
58
|
+
except (OSError, TypeError):
|
|
59
|
+
results[".cursor/mcp.json"] = False
|
|
60
|
+
|
|
61
|
+
# Claude Desktop locations differ by operating system.
|
|
62
|
+
home = Path.home()
|
|
63
|
+
if os.name == "nt":
|
|
64
|
+
claude_path = home / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
|
|
65
|
+
elif os.uname().sysname == "Darwin":
|
|
66
|
+
claude_path = home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
67
|
+
else:
|
|
68
|
+
claude_path = home / ".config" / "Claude" / "claude_desktop_config.json"
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
_merge_json(
|
|
72
|
+
claude_path,
|
|
73
|
+
"mcpServers",
|
|
74
|
+
{"sheet-unmess": {"command": "python", "args": ["-m", "sheet_unmess.mcp_server"]}},
|
|
75
|
+
)
|
|
76
|
+
results["claude_desktop"] = True
|
|
77
|
+
except (OSError, TypeError):
|
|
78
|
+
results["claude_desktop"] = False
|
|
79
|
+
return results
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Row Classification Engine.
|
|
3
|
+
Forensically categorizes every row into:
|
|
4
|
+
- TITLE_BANNER
|
|
5
|
+
- DATA
|
|
6
|
+
- SUBTOTAL
|
|
7
|
+
- GRAND_TOTAL
|
|
8
|
+
- FOOTNOTE
|
|
9
|
+
- BLANK_PADDING
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, List, Optional
|
|
15
|
+
from sheet_unmess.normalizer import parse_forensic_number
|
|
16
|
+
|
|
17
|
+
RE_GRAND_TOTAL = re.compile(
|
|
18
|
+
r"\b(grand\s*total|overall\s*total|gesamtsumme|total\s*general|solde\s*final)\b",
|
|
19
|
+
re.IGNORECASE,
|
|
20
|
+
)
|
|
21
|
+
RE_SUBTOTAL = re.compile(
|
|
22
|
+
r"\b(sub\s*total|subtotal|summe|zwischensumme|sous\s*total|totales?|total)\b",
|
|
23
|
+
re.IGNORECASE,
|
|
24
|
+
)
|
|
25
|
+
RE_FOOTNOTE = re.compile(
|
|
26
|
+
r"^\s*([*ā ā”§#^]|note:|notes:|source:|disclaimer:|confidential|prepared\s+by|copyright)",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RowClassifier:
|
|
32
|
+
@staticmethod
|
|
33
|
+
def is_blank(row: List[Any]) -> bool:
|
|
34
|
+
for cell in row:
|
|
35
|
+
if cell is not None and str(cell).strip() not in ("", "None", "nan"):
|
|
36
|
+
return False
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def is_footnote(row: List[Any], total_cols: int) -> bool:
|
|
41
|
+
# A footnote typically has content in the first 1-2 columns and is empty everywhere else
|
|
42
|
+
non_empty = [c for c in row if c is not None and str(c).strip() not in ("", "None", "nan")]
|
|
43
|
+
if not non_empty:
|
|
44
|
+
return False
|
|
45
|
+
first_str = str(non_empty[0]).strip()
|
|
46
|
+
if RE_FOOTNOTE.match(first_str):
|
|
47
|
+
return True
|
|
48
|
+
# If there is only 1 non-empty cell in a wide table, and it is a long text string (>30 chars)
|
|
49
|
+
if len(non_empty) == 1 and len(first_str) > 30 and total_cols >= 3:
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def classify_row(
|
|
55
|
+
cls,
|
|
56
|
+
row: List[Any],
|
|
57
|
+
row_idx: int,
|
|
58
|
+
header_end_idx: int,
|
|
59
|
+
total_rows: int,
|
|
60
|
+
col_names: Optional[List[str]] = None,
|
|
61
|
+
) -> str:
|
|
62
|
+
if cls.is_blank(row):
|
|
63
|
+
return "BLANK_PADDING"
|
|
64
|
+
|
|
65
|
+
# Rows prior to data block
|
|
66
|
+
if row_idx < header_end_idx:
|
|
67
|
+
return "TITLE_BANNER"
|
|
68
|
+
|
|
69
|
+
total_cols = len(row)
|
|
70
|
+
if cls.is_footnote(row, total_cols):
|
|
71
|
+
return "FOOTNOTE"
|
|
72
|
+
|
|
73
|
+
# Check for subtotal or grand total markers
|
|
74
|
+
first_cells_str = " ".join([str(c) for c in row[:3] if c is not None])
|
|
75
|
+
|
|
76
|
+
if RE_GRAND_TOTAL.search(first_cells_str):
|
|
77
|
+
return "GRAND_TOTAL"
|
|
78
|
+
|
|
79
|
+
if RE_SUBTOTAL.search(first_cells_str):
|
|
80
|
+
return "SUBTOTAL"
|
|
81
|
+
|
|
82
|
+
return "DATA"
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def extract_row_numerics(cls, row: List[Any], col_names: List[str]) -> dict[str, float]:
|
|
86
|
+
"""Extracts all parsable numbers from a row keyed by column name."""
|
|
87
|
+
numerics = {}
|
|
88
|
+
for c_idx, cell in enumerate(row):
|
|
89
|
+
if c_idx < len(col_names):
|
|
90
|
+
col = col_names[c_idx]
|
|
91
|
+
parsed, is_num = parse_forensic_number(cell)
|
|
92
|
+
if is_num and parsed is not None:
|
|
93
|
+
numerics[col] = float(parsed)
|
|
94
|
+
return numerics
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich Command-Line Interface for sheet-unmess.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from sheet_unmess.core import read_messy
|
|
10
|
+
from sheet_unmess.ai_rules import install_ai_rules
|
|
11
|
+
from sheet_unmess.mcp_server import main as mcp_main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="sheet-unmess",
|
|
17
|
+
description="Forensic Messy-Excel & Dirty-CSV Ingestor + Tamper-Evident Audit Receipt."
|
|
18
|
+
)
|
|
19
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
20
|
+
|
|
21
|
+
# Command: clean
|
|
22
|
+
clean_p = subparsers.add_parser("clean", help="Clean a messy spreadsheet")
|
|
23
|
+
clean_p.add_argument("file", help="Path to .xlsx or .csv")
|
|
24
|
+
clean_p.add_argument("--sheet", default=0, help="Sheet name or index (for Excel)")
|
|
25
|
+
clean_p.add_argument("--out", "-o", help="Output path (.csv or .parquet)")
|
|
26
|
+
clean_p.add_argument("--receipt", "-r", help="Save Audit Receipt markdown file path")
|
|
27
|
+
clean_p.add_argument("--dayfirst", action="store_true", help="Parse dates EU style (DD/MM/YYYY)")
|
|
28
|
+
|
|
29
|
+
# Command: init-ai
|
|
30
|
+
subparsers.add_parser("init-ai", help="Inject MCP server and rules into Cursor, Windsurf, and Claude")
|
|
31
|
+
|
|
32
|
+
# Command: mcp
|
|
33
|
+
subparsers.add_parser("mcp", help="Launch native Stdio Model Context Protocol (MCP) server")
|
|
34
|
+
|
|
35
|
+
args = parser.parse_args()
|
|
36
|
+
|
|
37
|
+
if args.command == "mcp":
|
|
38
|
+
mcp_main()
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if args.command == "init-ai":
|
|
42
|
+
res = install_ai_rules()
|
|
43
|
+
print("š Successfully provisioned AI agent configurations:")
|
|
44
|
+
for target, status in res.items():
|
|
45
|
+
icon = "ā
" if status else "ā ļø"
|
|
46
|
+
print(f" {icon} {target}")
|
|
47
|
+
print("\nYour AI assistant (Cursor, Windsurf, Claude) is now primed to automatically clean spreadsheets with sheet-unmess!")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if args.command == "clean":
|
|
51
|
+
file_path = Path(args.file)
|
|
52
|
+
if not file_path.exists():
|
|
53
|
+
print(f"ā File not found: {file_path}", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
print(f"š Forensically ingesting `{file_path}`...")
|
|
57
|
+
report = read_messy(
|
|
58
|
+
file_path,
|
|
59
|
+
sheet_name=args.sheet,
|
|
60
|
+
date_dayfirst=args.dayfirst,
|
|
61
|
+
return_report=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
df = report.data
|
|
65
|
+
receipt = report.receipt
|
|
66
|
+
|
|
67
|
+
print(f"⨠Ingestion complete: {receipt.raw_shape} ā {receipt.clean_shape} in {receipt.execution_time_ms:.1f}ms")
|
|
68
|
+
|
|
69
|
+
if receipt.reconciliations:
|
|
70
|
+
print("\nš Invariant Verification:")
|
|
71
|
+
for rec in receipt.reconciliations:
|
|
72
|
+
icon = "ā
" if rec.status in ("VERIFIED_EXACT", "ROUNDING_TOLERANCE") else "šØ"
|
|
73
|
+
print(f" {icon} {rec.column}: reported {rec.reported_total:,.2f} vs sum {rec.computed_sum:,.2f} ({rec.status})")
|
|
74
|
+
|
|
75
|
+
# Save output data
|
|
76
|
+
if args.out:
|
|
77
|
+
out_p = Path(args.out)
|
|
78
|
+
if out_p.suffix == ".parquet":
|
|
79
|
+
df.to_parquet(out_p, index=False)
|
|
80
|
+
else:
|
|
81
|
+
df.to_csv(out_p, index=False)
|
|
82
|
+
print(f"\nš¾ Clean data written to: {out_p}")
|
|
83
|
+
else:
|
|
84
|
+
print("\nPreview of cleaned data:")
|
|
85
|
+
print(df.head(10).to_string())
|
|
86
|
+
|
|
87
|
+
# Save receipt
|
|
88
|
+
if args.receipt:
|
|
89
|
+
rec_p = Path(args.receipt)
|
|
90
|
+
rec_p.write_text(receipt.to_markdown(), encoding="utf-8")
|
|
91
|
+
print(f"š”ļø Audit Receipt written to: {rec_p}")
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
parser.print_help()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|