dataassay 0.7.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.
- dataassay-0.7.0/.gitignore +8 -0
- dataassay-0.7.0/LICENSE +21 -0
- dataassay-0.7.0/PKG-INFO +208 -0
- dataassay-0.7.0/README.md +179 -0
- dataassay-0.7.0/pyproject.toml +62 -0
- dataassay-0.7.0/src/dataassay/__init__.py +11 -0
- dataassay-0.7.0/src/dataassay/__main__.py +4 -0
- dataassay-0.7.0/src/dataassay/audit.py +166 -0
- dataassay-0.7.0/src/dataassay/checks/base.py +217 -0
- dataassay-0.7.0/src/dataassay/checks/distribution.py +385 -0
- dataassay-0.7.0/src/dataassay/checks/precision.py +147 -0
- dataassay-0.7.0/src/dataassay/checks/registry.py +51 -0
- dataassay-0.7.0/src/dataassay/checks/structural.py +400 -0
- dataassay-0.7.0/src/dataassay/checks/temporal.py +605 -0
- dataassay-0.7.0/src/dataassay/cli.py +671 -0
- dataassay-0.7.0/src/dataassay/columns.py +389 -0
- dataassay-0.7.0/src/dataassay/llm/__init__.py +9 -0
- dataassay-0.7.0/src/dataassay/llm/interview.py +180 -0
- dataassay-0.7.0/src/dataassay/llm/payload.py +111 -0
- dataassay-0.7.0/src/dataassay/llm/provider.py +144 -0
- dataassay-0.7.0/src/dataassay/manifest.py +158 -0
- dataassay-0.7.0/src/dataassay/profile.py +377 -0
- dataassay-0.7.0/src/dataassay/provenance.py +155 -0
- dataassay-0.7.0/src/dataassay/rawscan.py +329 -0
- dataassay-0.7.0/src/dataassay/report/__init__.py +142 -0
- dataassay-0.7.0/src/dataassay/report/charts.py +326 -0
- dataassay-0.7.0/src/dataassay/report/evidence.py +155 -0
- dataassay-0.7.0/src/dataassay/report/html.py +356 -0
- dataassay-0.7.0/src/dataassay/structure.py +381 -0
- dataassay-0.7.0/tests/conftest.py +74 -0
- dataassay-0.7.0/tests/test_checks.py +252 -0
- dataassay-0.7.0/tests/test_cli.py +61 -0
- dataassay-0.7.0/tests/test_columns.py +132 -0
- dataassay-0.7.0/tests/test_llm.py +274 -0
- dataassay-0.7.0/tests/test_manifest.py +236 -0
- dataassay-0.7.0/tests/test_ordering.py +127 -0
- dataassay-0.7.0/tests/test_precision.py +94 -0
- dataassay-0.7.0/tests/test_profile.py +158 -0
- dataassay-0.7.0/tests/test_provenance.py +70 -0
- dataassay-0.7.0/tests/test_rawscan.py +119 -0
- dataassay-0.7.0/tests/test_report.py +141 -0
- dataassay-0.7.0/tests/test_structure.py +88 -0
dataassay-0.7.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SoftSignal LLC
|
|
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.
|
dataassay-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dataassay
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Audit a tabular dataset: characterize it, test what can be tested, and say plainly what could not be.
|
|
5
|
+
Project-URL: Homepage, https://getsoftsignal.com
|
|
6
|
+
Author: SoftSignal LLC
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: audit,csv,data-quality,duckdb,parquet,profiling
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: duckdb>=1.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
26
|
+
Provides-Extra: llm
|
|
27
|
+
Requires-Dist: anthropic>=0.40; extra == 'llm'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# dataassay
|
|
31
|
+
|
|
32
|
+
Audit a tabular dataset — locally.
|
|
33
|
+
|
|
34
|
+
An assay characterizes a sample before it makes any claim about it. This tool
|
|
35
|
+
works in the same order:
|
|
36
|
+
|
|
37
|
+
1. **Characterize.** Establish what kind of thing each column is — its support,
|
|
38
|
+
cadence, panel shape, whether a standard deviation can be established at all.
|
|
39
|
+
2. **Test.** Run only the checks that those properties make *valid*. A 3σ rule
|
|
40
|
+
on a heavy-tailed series is not a weak check; it is an invalid one.
|
|
41
|
+
3. **Grade.** Rank what came back, and separate real defects from source-side
|
|
42
|
+
bookkeeping that merely looks like one.
|
|
43
|
+
|
|
44
|
+
A check that could not be run is a deliverable. "Seasonality undetermined — only
|
|
45
|
+
1.2 cycles of history" is something you need to know, and it appears in the
|
|
46
|
+
report next to what did run.
|
|
47
|
+
|
|
48
|
+
## Status
|
|
49
|
+
|
|
50
|
+
**v0.7.0.** `assay audit --report out.html` writes a self-contained report and
|
|
51
|
+
a flagged-items CSV. `assay profile` characterizes; `assay catalog` prints what
|
|
52
|
+
the checks are; `assay init` writes a manifest and `assay declare` answers it;
|
|
53
|
+
`assay interview` optionally asks a model to propose answers, sending metadata
|
|
54
|
+
only.
|
|
55
|
+
|
|
56
|
+
Validated against a corpus of known defects (see `VALIDATION.md`) and swept
|
|
57
|
+
against 104 real CSVs from the pipeline it was built for, where the metric that
|
|
58
|
+
matters is defects per file rather than findings per file.
|
|
59
|
+
|
|
60
|
+
Thirteen checks, each earned by a real defect found in a production pipeline:
|
|
61
|
+
constant/all-zero columns, mojibake, duplicate rows, duplicate grain, future
|
|
62
|
+
dates, cadence gaps, file order, stale tails, flatline tails, saturation at a
|
|
63
|
+
bound, negligible residue, level shifts, and schema drift.
|
|
64
|
+
|
|
65
|
+
The observation date does not have to be a date column. Where it is spread
|
|
66
|
+
across `year`, `month` and `dekad`, or across `year` and a label like
|
|
67
|
+
`"END OF APR"`, it is assembled — otherwise five of the thirteen checks are
|
|
68
|
+
withheld on a whole class of agricultural and government files.
|
|
69
|
+
|
|
70
|
+
Precision is measured but never filed as a fault. A column where most values
|
|
71
|
+
carry 15+ significant digits has been computed, not reported — that is
|
|
72
|
+
provenance, and it belongs beside the column rather than in the findings.
|
|
73
|
+
|
|
74
|
+
Every check declares the properties it needs and is refused when the profile
|
|
75
|
+
has not established them. What was withheld and why travels beside the
|
|
76
|
+
findings, because zero findings at 30% coverage and zero at 95% are different
|
|
77
|
+
objects and one number cannot tell them apart.
|
|
78
|
+
|
|
79
|
+
Findings carry a disposition — likely defect, worth a look, or the source doing
|
|
80
|
+
its own bookkeeping — because most anomalies in real data are the source's own
|
|
81
|
+
calendar, and a tool that cannot say so gets switched off within a week. They
|
|
82
|
+
also carry the predicate that produced them, so any claim can be re-run without
|
|
83
|
+
this code.
|
|
84
|
+
|
|
85
|
+
Confidence is never an opaque score. Each input is named, and agreement between
|
|
86
|
+
two independent checks on one column promotes it.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
pip install dataassay
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Use
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
assay audit data.parquet # characterize, then check
|
|
98
|
+
assay profile data.csv # characterize only
|
|
99
|
+
assay init data.csv # write a manifest to answer its questions
|
|
100
|
+
assay declare data.csv --time-axis d # record an answer in it
|
|
101
|
+
assay catalog # what the checks are and why
|
|
102
|
+
assay audit data.csv --json # the machine contract
|
|
103
|
+
assay interview data.csv # optional: ask a model (metadata only)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Validation
|
|
107
|
+
|
|
108
|
+
It was tested against 82 findings from a real pipeline audit, on the files as
|
|
109
|
+
they stood *before* those findings were fixed, recovered from git — and on the
|
|
110
|
+
same files afterwards. See [VALIDATION.md](VALIDATION.md), including the
|
|
111
|
+
denominator (only ~30 of the 82 are visible in a data file at all) and the four
|
|
112
|
+
families still out of reach.
|
|
113
|
+
|
|
114
|
+
## The report
|
|
115
|
+
|
|
116
|
+
One HTML file. No CDN, no external request, no dependency — charts are inline
|
|
117
|
+
SVG drawn by hand, and the machine-readable findings are embedded in the page
|
|
118
|
+
rather than written beside it, so the human artifact and the machine contract
|
|
119
|
+
are the same bytes and cannot drift.
|
|
120
|
+
|
|
121
|
+
Ordered the way it has to be read: provenance, then **coverage before the
|
|
122
|
+
findings** (an empty findings list means nothing until you know how many checks
|
|
123
|
+
ran), then the findings — each with its governing property, one chart, the
|
|
124
|
+
composition of its confidence, and the predicate to re-run it without this code
|
|
125
|
+
— then what each column was taken to be, where a wrong assumption of ours is
|
|
126
|
+
visible, then every check including the ones that passed.
|
|
127
|
+
|
|
128
|
+
Charts are theme-aware and never lean on colour alone: a flagged point carries
|
|
129
|
+
a rule and a direct label, a flagged range is a band with one label, and any
|
|
130
|
+
chart with two categories carries a legend.
|
|
131
|
+
|
|
132
|
+
## The manifest
|
|
133
|
+
|
|
134
|
+
`assay init` writes `<file>.assay.json`: what the tool detected, the questions
|
|
135
|
+
it could not answer, and an empty `declared` block for you to fill in.
|
|
136
|
+
|
|
137
|
+
The two blocks are kept apart on purpose. `detected` is regenerated every time
|
|
138
|
+
and is only there for reference; `declared` is what a person says is true, and
|
|
139
|
+
it always wins. So a value's provenance is never ambiguous — there is no
|
|
140
|
+
guessing later whether a grain was inferred or confirmed, and the report can be
|
|
141
|
+
honest about which checks rest on an assumption and which rest on an answer.
|
|
142
|
+
|
|
143
|
+
Answering once is what makes the tool usable more than once. The next audit of
|
|
144
|
+
the same dataset asks nothing, and it can run where there is nobody to ask at
|
|
145
|
+
all — a pipeline, a server, CI. The conversation is just the most convenient
|
|
146
|
+
way to author the file the first time.
|
|
147
|
+
|
|
148
|
+
`assay declare` is how you answer without opening the file:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
assay declare data.csv --time-axis observed_at
|
|
152
|
+
assay declare data.csv --grain observed_at,site
|
|
153
|
+
assay declare data.csv --skip high_null # seen it, not answering
|
|
154
|
+
assay declare data.csv --accept-proposed # the model was right
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Running a command is a person deciding, so `declared` keeps meaning what it
|
|
158
|
+
says. Two things it refuses: a declaration naming a column the file does not
|
|
159
|
+
have (that would gate checks on a fiction), and promoting anything out of
|
|
160
|
+
`proposed` unless you pass `--accept-proposed` — the model never gets a quiet
|
|
161
|
+
path into `declared`.
|
|
162
|
+
|
|
163
|
+
A skipped question is not a hidden one. It moves out of the open list and into
|
|
164
|
+
a section of its own, because a question nobody has looked at and a question
|
|
165
|
+
someone has judged not worth answering are different states, and showing a
|
|
166
|
+
decision as outstanding work is how a report teaches people to stop reading
|
|
167
|
+
it.
|
|
168
|
+
|
|
169
|
+
## The interview (optional)
|
|
170
|
+
|
|
171
|
+
`assay interview` sends the **profile** — column names, types, counts,
|
|
172
|
+
quantiles, and the questions the profiler could not settle — to a model, and
|
|
173
|
+
writes what it proposes into the manifest.
|
|
174
|
+
|
|
175
|
+
Three things make it safe to use on data you cannot upload.
|
|
176
|
+
|
|
177
|
+
**It sends metadata, never rows.** You see exactly what would go, before it
|
|
178
|
+
goes: a plain-language summary, then a confirmation. `--show-payload` prints the
|
|
179
|
+
literal bytes and sends nothing. Two disclosures are admitted rather than
|
|
180
|
+
hidden — column *names* have to go, and low-cardinality columns include their
|
|
181
|
+
distinct values because a categorical column cannot be characterized without
|
|
182
|
+
them. `--redact-values` strips the second.
|
|
183
|
+
|
|
184
|
+
**Proposals never become answers.** They land in the manifest's `proposed`
|
|
185
|
+
block, beside `declared`, and are never applied. You move across what you agree
|
|
186
|
+
with. `declared` records that a person decided, and that is only worth
|
|
187
|
+
something if it stays true.
|
|
188
|
+
|
|
189
|
+
**The model does not choose which checks run.** It proposes property
|
|
190
|
+
declarations; the engine gates the checks itself, deterministically, exactly as
|
|
191
|
+
it does for a declaration you typed. Handing check selection to a language model
|
|
192
|
+
would throw away the discipline the whole tool is built on.
|
|
193
|
+
|
|
194
|
+
Credentials come from the environment only — never a file, never an argument,
|
|
195
|
+
so a key cannot end up in your shell history or a commit.
|
|
196
|
+
|
|
197
|
+
## Privacy
|
|
198
|
+
|
|
199
|
+
Nothing is uploaded. The engine runs entirely in your process against your
|
|
200
|
+
files, and has exactly one runtime dependency (`duckdb`, which has no
|
|
201
|
+
transitive dependencies of its own). The LLM adapter is an optional extra
|
|
202
|
+
(`pip install 'dataassay[llm]'`) and the only module in the package that opens a
|
|
203
|
+
network connection — a test walks every other module's imports and fails if a
|
|
204
|
+
network library appears in one.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT © SoftSignal LLC
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# dataassay
|
|
2
|
+
|
|
3
|
+
Audit a tabular dataset — locally.
|
|
4
|
+
|
|
5
|
+
An assay characterizes a sample before it makes any claim about it. This tool
|
|
6
|
+
works in the same order:
|
|
7
|
+
|
|
8
|
+
1. **Characterize.** Establish what kind of thing each column is — its support,
|
|
9
|
+
cadence, panel shape, whether a standard deviation can be established at all.
|
|
10
|
+
2. **Test.** Run only the checks that those properties make *valid*. A 3σ rule
|
|
11
|
+
on a heavy-tailed series is not a weak check; it is an invalid one.
|
|
12
|
+
3. **Grade.** Rank what came back, and separate real defects from source-side
|
|
13
|
+
bookkeeping that merely looks like one.
|
|
14
|
+
|
|
15
|
+
A check that could not be run is a deliverable. "Seasonality undetermined — only
|
|
16
|
+
1.2 cycles of history" is something you need to know, and it appears in the
|
|
17
|
+
report next to what did run.
|
|
18
|
+
|
|
19
|
+
## Status
|
|
20
|
+
|
|
21
|
+
**v0.7.0.** `assay audit --report out.html` writes a self-contained report and
|
|
22
|
+
a flagged-items CSV. `assay profile` characterizes; `assay catalog` prints what
|
|
23
|
+
the checks are; `assay init` writes a manifest and `assay declare` answers it;
|
|
24
|
+
`assay interview` optionally asks a model to propose answers, sending metadata
|
|
25
|
+
only.
|
|
26
|
+
|
|
27
|
+
Validated against a corpus of known defects (see `VALIDATION.md`) and swept
|
|
28
|
+
against 104 real CSVs from the pipeline it was built for, where the metric that
|
|
29
|
+
matters is defects per file rather than findings per file.
|
|
30
|
+
|
|
31
|
+
Thirteen checks, each earned by a real defect found in a production pipeline:
|
|
32
|
+
constant/all-zero columns, mojibake, duplicate rows, duplicate grain, future
|
|
33
|
+
dates, cadence gaps, file order, stale tails, flatline tails, saturation at a
|
|
34
|
+
bound, negligible residue, level shifts, and schema drift.
|
|
35
|
+
|
|
36
|
+
The observation date does not have to be a date column. Where it is spread
|
|
37
|
+
across `year`, `month` and `dekad`, or across `year` and a label like
|
|
38
|
+
`"END OF APR"`, it is assembled — otherwise five of the thirteen checks are
|
|
39
|
+
withheld on a whole class of agricultural and government files.
|
|
40
|
+
|
|
41
|
+
Precision is measured but never filed as a fault. A column where most values
|
|
42
|
+
carry 15+ significant digits has been computed, not reported — that is
|
|
43
|
+
provenance, and it belongs beside the column rather than in the findings.
|
|
44
|
+
|
|
45
|
+
Every check declares the properties it needs and is refused when the profile
|
|
46
|
+
has not established them. What was withheld and why travels beside the
|
|
47
|
+
findings, because zero findings at 30% coverage and zero at 95% are different
|
|
48
|
+
objects and one number cannot tell them apart.
|
|
49
|
+
|
|
50
|
+
Findings carry a disposition — likely defect, worth a look, or the source doing
|
|
51
|
+
its own bookkeeping — because most anomalies in real data are the source's own
|
|
52
|
+
calendar, and a tool that cannot say so gets switched off within a week. They
|
|
53
|
+
also carry the predicate that produced them, so any claim can be re-run without
|
|
54
|
+
this code.
|
|
55
|
+
|
|
56
|
+
Confidence is never an opaque score. Each input is named, and agreement between
|
|
57
|
+
two independent checks on one column promotes it.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
pip install dataassay
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Use
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
assay audit data.parquet # characterize, then check
|
|
69
|
+
assay profile data.csv # characterize only
|
|
70
|
+
assay init data.csv # write a manifest to answer its questions
|
|
71
|
+
assay declare data.csv --time-axis d # record an answer in it
|
|
72
|
+
assay catalog # what the checks are and why
|
|
73
|
+
assay audit data.csv --json # the machine contract
|
|
74
|
+
assay interview data.csv # optional: ask a model (metadata only)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Validation
|
|
78
|
+
|
|
79
|
+
It was tested against 82 findings from a real pipeline audit, on the files as
|
|
80
|
+
they stood *before* those findings were fixed, recovered from git — and on the
|
|
81
|
+
same files afterwards. See [VALIDATION.md](VALIDATION.md), including the
|
|
82
|
+
denominator (only ~30 of the 82 are visible in a data file at all) and the four
|
|
83
|
+
families still out of reach.
|
|
84
|
+
|
|
85
|
+
## The report
|
|
86
|
+
|
|
87
|
+
One HTML file. No CDN, no external request, no dependency — charts are inline
|
|
88
|
+
SVG drawn by hand, and the machine-readable findings are embedded in the page
|
|
89
|
+
rather than written beside it, so the human artifact and the machine contract
|
|
90
|
+
are the same bytes and cannot drift.
|
|
91
|
+
|
|
92
|
+
Ordered the way it has to be read: provenance, then **coverage before the
|
|
93
|
+
findings** (an empty findings list means nothing until you know how many checks
|
|
94
|
+
ran), then the findings — each with its governing property, one chart, the
|
|
95
|
+
composition of its confidence, and the predicate to re-run it without this code
|
|
96
|
+
— then what each column was taken to be, where a wrong assumption of ours is
|
|
97
|
+
visible, then every check including the ones that passed.
|
|
98
|
+
|
|
99
|
+
Charts are theme-aware and never lean on colour alone: a flagged point carries
|
|
100
|
+
a rule and a direct label, a flagged range is a band with one label, and any
|
|
101
|
+
chart with two categories carries a legend.
|
|
102
|
+
|
|
103
|
+
## The manifest
|
|
104
|
+
|
|
105
|
+
`assay init` writes `<file>.assay.json`: what the tool detected, the questions
|
|
106
|
+
it could not answer, and an empty `declared` block for you to fill in.
|
|
107
|
+
|
|
108
|
+
The two blocks are kept apart on purpose. `detected` is regenerated every time
|
|
109
|
+
and is only there for reference; `declared` is what a person says is true, and
|
|
110
|
+
it always wins. So a value's provenance is never ambiguous — there is no
|
|
111
|
+
guessing later whether a grain was inferred or confirmed, and the report can be
|
|
112
|
+
honest about which checks rest on an assumption and which rest on an answer.
|
|
113
|
+
|
|
114
|
+
Answering once is what makes the tool usable more than once. The next audit of
|
|
115
|
+
the same dataset asks nothing, and it can run where there is nobody to ask at
|
|
116
|
+
all — a pipeline, a server, CI. The conversation is just the most convenient
|
|
117
|
+
way to author the file the first time.
|
|
118
|
+
|
|
119
|
+
`assay declare` is how you answer without opening the file:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
assay declare data.csv --time-axis observed_at
|
|
123
|
+
assay declare data.csv --grain observed_at,site
|
|
124
|
+
assay declare data.csv --skip high_null # seen it, not answering
|
|
125
|
+
assay declare data.csv --accept-proposed # the model was right
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Running a command is a person deciding, so `declared` keeps meaning what it
|
|
129
|
+
says. Two things it refuses: a declaration naming a column the file does not
|
|
130
|
+
have (that would gate checks on a fiction), and promoting anything out of
|
|
131
|
+
`proposed` unless you pass `--accept-proposed` — the model never gets a quiet
|
|
132
|
+
path into `declared`.
|
|
133
|
+
|
|
134
|
+
A skipped question is not a hidden one. It moves out of the open list and into
|
|
135
|
+
a section of its own, because a question nobody has looked at and a question
|
|
136
|
+
someone has judged not worth answering are different states, and showing a
|
|
137
|
+
decision as outstanding work is how a report teaches people to stop reading
|
|
138
|
+
it.
|
|
139
|
+
|
|
140
|
+
## The interview (optional)
|
|
141
|
+
|
|
142
|
+
`assay interview` sends the **profile** — column names, types, counts,
|
|
143
|
+
quantiles, and the questions the profiler could not settle — to a model, and
|
|
144
|
+
writes what it proposes into the manifest.
|
|
145
|
+
|
|
146
|
+
Three things make it safe to use on data you cannot upload.
|
|
147
|
+
|
|
148
|
+
**It sends metadata, never rows.** You see exactly what would go, before it
|
|
149
|
+
goes: a plain-language summary, then a confirmation. `--show-payload` prints the
|
|
150
|
+
literal bytes and sends nothing. Two disclosures are admitted rather than
|
|
151
|
+
hidden — column *names* have to go, and low-cardinality columns include their
|
|
152
|
+
distinct values because a categorical column cannot be characterized without
|
|
153
|
+
them. `--redact-values` strips the second.
|
|
154
|
+
|
|
155
|
+
**Proposals never become answers.** They land in the manifest's `proposed`
|
|
156
|
+
block, beside `declared`, and are never applied. You move across what you agree
|
|
157
|
+
with. `declared` records that a person decided, and that is only worth
|
|
158
|
+
something if it stays true.
|
|
159
|
+
|
|
160
|
+
**The model does not choose which checks run.** It proposes property
|
|
161
|
+
declarations; the engine gates the checks itself, deterministically, exactly as
|
|
162
|
+
it does for a declaration you typed. Handing check selection to a language model
|
|
163
|
+
would throw away the discipline the whole tool is built on.
|
|
164
|
+
|
|
165
|
+
Credentials come from the environment only — never a file, never an argument,
|
|
166
|
+
so a key cannot end up in your shell history or a commit.
|
|
167
|
+
|
|
168
|
+
## Privacy
|
|
169
|
+
|
|
170
|
+
Nothing is uploaded. The engine runs entirely in your process against your
|
|
171
|
+
files, and has exactly one runtime dependency (`duckdb`, which has no
|
|
172
|
+
transitive dependencies of its own). The LLM adapter is an optional extra
|
|
173
|
+
(`pip install 'dataassay[llm]'`) and the only module in the package that opens a
|
|
174
|
+
network connection — a test walks every other module's imports and fails if a
|
|
175
|
+
network library appears in one.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT © SoftSignal LLC
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dataassay"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Audit a tabular dataset: characterize it, test what can be tested, and say plainly what could not be."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "SoftSignal LLC" }]
|
|
14
|
+
keywords = ["data-quality", "audit", "profiling", "parquet", "csv", "duckdb"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# ONE runtime dependency, deliberately. duckdb has zero transitive deps and
|
|
29
|
+
# ships prebuilt wheels everywhere. Anything added here is a support burden on
|
|
30
|
+
# every machine this is ever installed on -- and the dependency tree is part of
|
|
31
|
+
# the pitch to the buyer who will not upload their data in the first place.
|
|
32
|
+
dependencies = ["duckdb>=1.0"]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
llm = ["anthropic>=0.40"] # The ONLY place a network dependency appears.
|
|
36
|
+
dev = ["pytest>=8", "ruff>=0.6", "build>=1.2"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
assay = "dataassay.cli:main"
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://getsoftsignal.com"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.version]
|
|
45
|
+
path = "src/dataassay/__init__.py"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/dataassay"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = ["src/", "tests/", "README.md", "LICENSE"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
addopts = "-q"
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = "py310"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""dataassay — audit a tabular dataset.
|
|
2
|
+
|
|
3
|
+
An assay characterizes a sample before it makes any claim about it. This tool
|
|
4
|
+
works in that order too: establish what kind of thing each column is, run only
|
|
5
|
+
the checks that property makes valid, and report what could NOT be checked as
|
|
6
|
+
plainly as what failed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.7.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Orchestration: profile, infer structure, gate the checks, rank what survives.
|
|
2
|
+
|
|
3
|
+
Coverage is a first-class output, not a footnote. A reader cannot interpret an
|
|
4
|
+
empty findings list without knowing how many checks ran, which were withheld,
|
|
5
|
+
and which are waiting on an answer -- zero findings at 30% coverage and zero at
|
|
6
|
+
95% are entirely different objects, and one number cannot tell them apart.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import duckdb
|
|
15
|
+
|
|
16
|
+
from dataassay import manifest as manifest_mod
|
|
17
|
+
from dataassay import structure as structure_mod
|
|
18
|
+
from dataassay.checks.base import Confidence, Finding
|
|
19
|
+
from dataassay.checks.registry import CATALOG, CATALOG_VERSION
|
|
20
|
+
from dataassay.profile import Profile, build
|
|
21
|
+
from dataassay.provenance import reader_for, source_expr
|
|
22
|
+
from dataassay.structure import Structure
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Coverage:
|
|
27
|
+
ran: list[str] = field(default_factory=list)
|
|
28
|
+
withheld: list[tuple[str, str]] = field(default_factory=list)
|
|
29
|
+
blocked: list[tuple[str, str]] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def total(self) -> int:
|
|
33
|
+
return len(self.ran) + len(self.withheld) + len(self.blocked)
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
return {
|
|
37
|
+
"checks_total": self.total,
|
|
38
|
+
"ran": self.ran,
|
|
39
|
+
"withheld": [{"check": c, "reason": r} for c, r in self.withheld],
|
|
40
|
+
"blocked": [{"check": c, "question": qn} for c, qn in self.blocked],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Audit:
|
|
46
|
+
profile: Profile
|
|
47
|
+
structure: Structure
|
|
48
|
+
findings: list[Finding]
|
|
49
|
+
coverage: Coverage
|
|
50
|
+
catalog_version: str = CATALOG_VERSION
|
|
51
|
+
manifest_path: str | None = None
|
|
52
|
+
# Question codes a person has seen and deliberately declined to answer.
|
|
53
|
+
# An unanswered question and a declined one are different states and the
|
|
54
|
+
# report must not show them as the same thing: the first is work nobody has
|
|
55
|
+
# done yet, the second is a decision, and presenting a decision as an
|
|
56
|
+
# outstanding task is how a report trains people to ignore it.
|
|
57
|
+
skipped_questions: list[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def open_questions(self) -> list:
|
|
61
|
+
return [q for q in self.profile.questions
|
|
62
|
+
if q.code not in set(self.skipped_questions)]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def declined_questions(self) -> list:
|
|
66
|
+
return [q for q in self.profile.questions
|
|
67
|
+
if q.code in set(self.skipped_questions)]
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
d = self.profile.to_dict()
|
|
71
|
+
d |= {
|
|
72
|
+
"catalog_version": self.catalog_version,
|
|
73
|
+
"structure": self.structure.to_dict(),
|
|
74
|
+
"coverage": self.coverage.to_dict(),
|
|
75
|
+
"manifest": self.manifest_path,
|
|
76
|
+
"skipped_questions": self.skipped_questions,
|
|
77
|
+
"findings": [f.to_dict() for f in self.findings],
|
|
78
|
+
}
|
|
79
|
+
return d
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _corroborate(findings: list[Finding]) -> list[Finding]:
|
|
83
|
+
"""Promote findings that two independent checks agree on.
|
|
84
|
+
|
|
85
|
+
Agreement is the strongest confidence signal available, and the only one
|
|
86
|
+
that does not depend on trusting a single detector's threshold. A column
|
|
87
|
+
flagged by both a saturation check and a level-shift check is real in a way
|
|
88
|
+
that either alone is not.
|
|
89
|
+
"""
|
|
90
|
+
by_column: dict[str, set[str]] = {}
|
|
91
|
+
for f in findings:
|
|
92
|
+
if f.column:
|
|
93
|
+
by_column.setdefault(f.column, set()).add(f.check_id)
|
|
94
|
+
|
|
95
|
+
out = []
|
|
96
|
+
for f in findings:
|
|
97
|
+
others = by_column.get(f.column or "", set()) - {f.check_id}
|
|
98
|
+
if others and f.confidence.level != "high":
|
|
99
|
+
f = Finding(
|
|
100
|
+
check_id=f.check_id,
|
|
101
|
+
column=f.column,
|
|
102
|
+
disposition=f.disposition,
|
|
103
|
+
summary=f.summary,
|
|
104
|
+
evidence=f.evidence,
|
|
105
|
+
predicate=f.predicate,
|
|
106
|
+
confidence=f.confidence.corroborated_by(", ".join(sorted(others))),
|
|
107
|
+
raw_values=f.raw_values,
|
|
108
|
+
)
|
|
109
|
+
out.append(f)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run(
|
|
114
|
+
path: Path,
|
|
115
|
+
byte_cap: int | None = None,
|
|
116
|
+
manifest_path: Path | None = None,
|
|
117
|
+
use_manifest: bool = True,
|
|
118
|
+
) -> Audit:
|
|
119
|
+
from dataassay.checks.base import CheckContext
|
|
120
|
+
from dataassay.rawscan import BYTE_CAP
|
|
121
|
+
|
|
122
|
+
reader = reader_for(path)
|
|
123
|
+
con = duckdb.connect(":memory:")
|
|
124
|
+
try:
|
|
125
|
+
profile = build(path, byte_cap=byte_cap or BYTE_CAP, con=con)
|
|
126
|
+
source = source_expr(reader, profile.provenance.read_mode)
|
|
127
|
+
params = [str(path)]
|
|
128
|
+
manifest = (
|
|
129
|
+
manifest_mod.discover(path, manifest_path) if use_manifest else None
|
|
130
|
+
)
|
|
131
|
+
struct = structure_mod.infer(
|
|
132
|
+
profile.columns, con, source, params, profile.provenance.row_count,
|
|
133
|
+
manifest=manifest,
|
|
134
|
+
)
|
|
135
|
+
ctx = CheckContext(
|
|
136
|
+
profile=profile, structure=struct, con=con, source=source,
|
|
137
|
+
params=params, manifest=manifest,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
coverage = Coverage()
|
|
141
|
+
findings: list[Finding] = []
|
|
142
|
+
for check in CATALOG:
|
|
143
|
+
verdict = check.applies(ctx)
|
|
144
|
+
if not verdict.applicable:
|
|
145
|
+
if verdict.blocked:
|
|
146
|
+
coverage.blocked.append((check.spec.id, verdict.reason))
|
|
147
|
+
else:
|
|
148
|
+
coverage.withheld.append((check.spec.id, verdict.reason))
|
|
149
|
+
continue
|
|
150
|
+
coverage.ran.append(check.spec.id)
|
|
151
|
+
findings.extend(check.run(ctx))
|
|
152
|
+
finally:
|
|
153
|
+
con.close()
|
|
154
|
+
|
|
155
|
+
findings = _corroborate(findings)
|
|
156
|
+
findings.sort(key=lambda f: f.sort_key)
|
|
157
|
+
return Audit(
|
|
158
|
+
profile=profile, structure=struct, findings=findings, coverage=coverage,
|
|
159
|
+
manifest_path=(
|
|
160
|
+
str(manifest.source_path) if manifest and manifest.source_path else None
|
|
161
|
+
),
|
|
162
|
+
skipped_questions=list(manifest.skipped) if manifest else [],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
__all__ = ["Audit", "Coverage", "Confidence", "run"]
|