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.
Files changed (42) hide show
  1. dataassay-0.7.0/.gitignore +8 -0
  2. dataassay-0.7.0/LICENSE +21 -0
  3. dataassay-0.7.0/PKG-INFO +208 -0
  4. dataassay-0.7.0/README.md +179 -0
  5. dataassay-0.7.0/pyproject.toml +62 -0
  6. dataassay-0.7.0/src/dataassay/__init__.py +11 -0
  7. dataassay-0.7.0/src/dataassay/__main__.py +4 -0
  8. dataassay-0.7.0/src/dataassay/audit.py +166 -0
  9. dataassay-0.7.0/src/dataassay/checks/base.py +217 -0
  10. dataassay-0.7.0/src/dataassay/checks/distribution.py +385 -0
  11. dataassay-0.7.0/src/dataassay/checks/precision.py +147 -0
  12. dataassay-0.7.0/src/dataassay/checks/registry.py +51 -0
  13. dataassay-0.7.0/src/dataassay/checks/structural.py +400 -0
  14. dataassay-0.7.0/src/dataassay/checks/temporal.py +605 -0
  15. dataassay-0.7.0/src/dataassay/cli.py +671 -0
  16. dataassay-0.7.0/src/dataassay/columns.py +389 -0
  17. dataassay-0.7.0/src/dataassay/llm/__init__.py +9 -0
  18. dataassay-0.7.0/src/dataassay/llm/interview.py +180 -0
  19. dataassay-0.7.0/src/dataassay/llm/payload.py +111 -0
  20. dataassay-0.7.0/src/dataassay/llm/provider.py +144 -0
  21. dataassay-0.7.0/src/dataassay/manifest.py +158 -0
  22. dataassay-0.7.0/src/dataassay/profile.py +377 -0
  23. dataassay-0.7.0/src/dataassay/provenance.py +155 -0
  24. dataassay-0.7.0/src/dataassay/rawscan.py +329 -0
  25. dataassay-0.7.0/src/dataassay/report/__init__.py +142 -0
  26. dataassay-0.7.0/src/dataassay/report/charts.py +326 -0
  27. dataassay-0.7.0/src/dataassay/report/evidence.py +155 -0
  28. dataassay-0.7.0/src/dataassay/report/html.py +356 -0
  29. dataassay-0.7.0/src/dataassay/structure.py +381 -0
  30. dataassay-0.7.0/tests/conftest.py +74 -0
  31. dataassay-0.7.0/tests/test_checks.py +252 -0
  32. dataassay-0.7.0/tests/test_cli.py +61 -0
  33. dataassay-0.7.0/tests/test_columns.py +132 -0
  34. dataassay-0.7.0/tests/test_llm.py +274 -0
  35. dataassay-0.7.0/tests/test_manifest.py +236 -0
  36. dataassay-0.7.0/tests/test_ordering.py +127 -0
  37. dataassay-0.7.0/tests/test_precision.py +94 -0
  38. dataassay-0.7.0/tests/test_profile.py +158 -0
  39. dataassay-0.7.0/tests/test_provenance.py +70 -0
  40. dataassay-0.7.0/tests/test_rawscan.py +119 -0
  41. dataassay-0.7.0/tests/test_report.py +141 -0
  42. dataassay-0.7.0/tests/test_structure.py +88 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -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.
@@ -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,4 @@
1
+ from dataassay.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -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"]