tabalyst 0.3.0__tar.gz → 0.4.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.
- {tabalyst-0.3.0/src/tabalyst.egg-info → tabalyst-0.4.0}/PKG-INFO +79 -14
- {tabalyst-0.3.0 → tabalyst-0.4.0}/README.md +76 -11
- {tabalyst-0.3.0 → tabalyst-0.4.0}/examples/README.md +12 -4
- tabalyst-0.4.0/examples/config.json +60 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/pyproject.toml +6 -3
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/__init__.py +10 -4
- tabalyst-0.4.0/src/tabalyst/batch.py +157 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/cli/app.py +2 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/cli/report.py +48 -63
- tabalyst-0.4.0/src/tabalyst/cli/scan.py +180 -0
- tabalyst-0.4.0/src/tabalyst/cli/terminal.py +56 -0
- tabalyst-0.4.0/src/tabalyst/config.py +262 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/execution_log.py +6 -4
- tabalyst-0.4.0/src/tabalyst/models.py +386 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/progress.py +7 -0
- tabalyst-0.4.0/src/tabalyst/report_config.py +72 -0
- tabalyst-0.4.0/src/tabalyst/report_profile.py +1084 -0
- tabalyst-0.4.0/src/tabalyst/report_service.py +268 -0
- tabalyst-0.4.0/src/tabalyst/reporting.py +130 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/sampling.py +2 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/sampling_service.py +15 -54
- tabalyst-0.4.0/src/tabalyst/scan_reuse.py +147 -0
- tabalyst-0.4.0/src/tabalyst/scan_service.py +184 -0
- tabalyst-0.4.0/src/tabalyst/scanner/__init__.py +12 -0
- tabalyst-0.4.0/src/tabalyst/scanner/api.py +175 -0
- tabalyst-0.4.0/src/tabalyst/scanner/config.py +519 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/__init__.py +16 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/amount.py +77 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/base.py +172 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/boolean.py +69 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/currency.py +134 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/email.py +86 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/enumeration.py +76 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/ip.py +128 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/names.py +87 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/number.py +161 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/pattern.py +48 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/percentage.py +43 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/phone.py +140 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/postal.py +99 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/quantity.py +100 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/registry.py +445 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/shape.py +36 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/temporal.py +399 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/url.py +144 -0
- tabalyst-0.4.0/src/tabalyst/scanner/detectors/uuid.py +97 -0
- tabalyst-0.4.0/src/tabalyst/scanner/diagnostics.py +82 -0
- tabalyst-0.4.0/src/tabalyst/scanner/engine.py +172 -0
- tabalyst-0.4.0/src/tabalyst/scanner/exposure.py +102 -0
- tabalyst-0.4.0/src/tabalyst/scanner/field.py +144 -0
- tabalyst-0.4.0/src/tabalyst/scanner/measures.py +452 -0
- tabalyst-0.4.0/src/tabalyst/scanner/models.py +525 -0
- tabalyst-0.4.0/src/tabalyst/scanner/normalization.py +99 -0
- tabalyst-0.4.0/src/tabalyst/scanner/observations.py +117 -0
- tabalyst-0.4.0/src/tabalyst/scanner/paths.py +142 -0
- tabalyst-0.4.0/src/tabalyst/scanner/readers/__init__.py +1 -0
- tabalyst-0.4.0/src/tabalyst/scanner/readers/base.py +78 -0
- tabalyst-0.4.0/src/tabalyst/scanner/readers/csv_reader.py +158 -0
- tabalyst-0.4.0/src/tabalyst/scanner/readers/json_reader.py +532 -0
- tabalyst-0.4.0/src/tabalyst/scanner/records.py +227 -0
- tabalyst-0.4.0/src/tabalyst/scanner/structure.py +320 -0
- tabalyst-0.4.0/src/tabalyst/scanner/technical.py +77 -0
- tabalyst-0.4.0/src/tabalyst/scanner/values.py +558 -0
- tabalyst-0.4.0/src/tabalyst/service.py +325 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/static/report.js +17 -3
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/static/theme.css +24 -0
- tabalyst-0.4.0/src/tabalyst/templates/report.html +108 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0/src/tabalyst.egg-info}/PKG-INFO +79 -14
- tabalyst-0.4.0/src/tabalyst.egg-info/SOURCES.txt +95 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst.egg-info/requires.txt +2 -2
- {tabalyst-0.3.0 → tabalyst-0.4.0}/tests/test_cli.py +17 -13
- {tabalyst-0.3.0 → tabalyst-0.4.0}/tests/test_public_api.py +2 -2
- {tabalyst-0.3.0 → tabalyst-0.4.0}/tests/test_reporting.py +4 -4
- {tabalyst-0.3.0 → tabalyst-0.4.0}/tests/test_sampling.py +16 -0
- tabalyst-0.4.0/tests/test_scan_command.py +416 -0
- tabalyst-0.4.0/tests/test_scanner_amounts.py +618 -0
- tabalyst-0.4.0/tests/test_scanner_config.py +85 -0
- tabalyst-0.4.0/tests/test_scanner_csv.py +170 -0
- tabalyst-0.4.0/tests/test_scanner_email_url.py +432 -0
- tabalyst-0.4.0/tests/test_scanner_exposure.py +263 -0
- tabalyst-0.4.0/tests/test_scanner_paths.py +72 -0
- tabalyst-0.4.0/tests/test_scanner_phone.py +308 -0
- tabalyst-0.4.0/tests/test_scanner_postal.py +270 -0
- tabalyst-0.4.0/tests/test_scanner_uuid_ip.py +518 -0
- tabalyst-0.3.0/examples/config.json +0 -48
- tabalyst-0.3.0/src/tabalyst/analysis.py +0 -883
- tabalyst-0.3.0/src/tabalyst/config.py +0 -214
- tabalyst-0.3.0/src/tabalyst/ingestion.py +0 -86
- tabalyst-0.3.0/src/tabalyst/models.py +0 -211
- tabalyst-0.3.0/src/tabalyst/report_service.py +0 -244
- tabalyst-0.3.0/src/tabalyst/reporting.py +0 -73
- tabalyst-0.3.0/src/tabalyst/service.py +0 -182
- tabalyst-0.3.0/src/tabalyst/templates/report.html +0 -83
- tabalyst-0.3.0/src/tabalyst.egg-info/SOURCES.txt +0 -41
- tabalyst-0.3.0/tests/test_analysis.py +0 -621
- {tabalyst-0.3.0 → tabalyst-0.4.0}/LICENSE +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/MANIFEST.in +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/examples/input/basic.csv +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/setup.cfg +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/cli/sample.py +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.3.0 → tabalyst-0.4.0}/src/tabalyst.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabalyst
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Open-source toolkit for understanding and working with unfamiliar data.
|
|
5
5
|
Author: Gregory Borelli
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,9 +21,9 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
+
Requires-Dist: ijson>=3.3
|
|
24
25
|
Requires-Dist: jinja2>=3.1
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist: pydantic>=2.10
|
|
26
|
+
Requires-Dist: pydantic>=2.11
|
|
27
27
|
Requires-Dist: typer>=0.12
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
@@ -65,6 +65,8 @@ changes often: update it before each new test.
|
|
|
65
65
|
| One file, custom name | `tabalyst report data.csv -o report.html` | The file `report.html` |
|
|
66
66
|
| Several files, automatic names | `tabalyst report *.csv` | Beside each source |
|
|
67
67
|
| Several files, one directory | `tabalyst report *.csv -d reports/` | The directory `reports/` |
|
|
68
|
+
| One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
|
|
69
|
+
| From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
|
|
68
70
|
|
|
69
71
|
The simplest command keeps the source filename:
|
|
70
72
|
|
|
@@ -87,6 +89,18 @@ Use `-o` to choose a different HTML filename for one source:
|
|
|
87
89
|
tabalyst report customers.csv -o customer-analysis.html
|
|
88
90
|
```
|
|
89
91
|
|
|
92
|
+
### JSON files
|
|
93
|
+
|
|
94
|
+
A JSON file gets a report too, named `<stem>.report.html` so its profile
|
|
95
|
+
`<stem>.report.json` never replaces the source. Each collection of records,
|
|
96
|
+
such as the `customers` array of `{"customers": [...]}`, is a dataset of the
|
|
97
|
+
report, and nested fields are columns named by their path, such as
|
|
98
|
+
`address.city`:
|
|
99
|
+
|
|
100
|
+
```console
|
|
101
|
+
tabalyst report orders.json
|
|
102
|
+
```
|
|
103
|
+
|
|
90
104
|
### Multiple files
|
|
91
105
|
|
|
92
106
|
Report several CSV files at once:
|
|
@@ -201,16 +215,53 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
|
|
|
201
215
|
only the requested sample, plus stratum counts, in memory. Existing outputs
|
|
202
216
|
require `--force`, and an input file is never overwritten.
|
|
203
217
|
|
|
218
|
+
## Tabalyst Scan
|
|
219
|
+
|
|
220
|
+
Describe every field of a CSV or JSON file in one JSON document:
|
|
221
|
+
|
|
222
|
+
```console
|
|
223
|
+
tabalyst scan customers.csv
|
|
224
|
+
tabalyst scan orders.json --collection "$.customers[]"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The default output is `customers.scan.json` beside the source; `-o`, `-d` and
|
|
228
|
+
`--force` work as for the other commands. Tabalyst Scan reads the file once as
|
|
229
|
+
a stream, with memory bounded by configurable limits, and records for each
|
|
230
|
+
field its presence, native types, missing values, frequencies, exact
|
|
231
|
+
statistics, normalization variants, technical type and the result of every
|
|
232
|
+
detector: numbers with decimal commas, dates, booleans, enumerations, email
|
|
233
|
+
addresses, URLs, phone numbers, postal codes, currency amounts, percentages,
|
|
234
|
+
quantities, UUIDs and IP addresses. Values of sensitive fields, such as email
|
|
235
|
+
addresses, are masked by default. Results are written atomically: an
|
|
236
|
+
interrupted scan never leaves a partial file.
|
|
237
|
+
|
|
238
|
+
Build the report from a scan document without reading the source again:
|
|
239
|
+
|
|
240
|
+
```console
|
|
241
|
+
tabalyst report --scan customers.scan.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
245
|
+
settings differ from the `scan` settings of `--config`.
|
|
246
|
+
|
|
247
|
+
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
248
|
+
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
249
|
+
|
|
204
250
|
## What the report analyzes
|
|
205
251
|
|
|
206
252
|
- Dataset dimensions, missing cells, duplicates, and quality observations.
|
|
207
253
|
- Physical and semantic types with confidence and error rates.
|
|
208
254
|
- Numeric, date, string-length, normalization, and value distributions.
|
|
209
|
-
- Distinct values, representative examples,
|
|
255
|
+
- Distinct values, representative examples, date formats, and semantic types
|
|
256
|
+
such as enumerations, email addresses, phone numbers, and postal codes.
|
|
210
257
|
- CSV record widths, quoting, encoding, and delimiter configuration.
|
|
211
258
|
- A bounded raw-data preview while every record is analyzed.
|
|
212
259
|
|
|
213
|
-
The
|
|
260
|
+
The report is built on Tabalyst Scan: it reads the CSV once as a stream and
|
|
261
|
+
masks values of sensitive columns, such as email addresses, by default.
|
|
262
|
+
Ambiguous dates stay ambiguous; the report shows the evidence of the column
|
|
263
|
+
without applying it. The HTML report is self-contained and works without a CDN
|
|
264
|
+
or network connection.
|
|
214
265
|
The JSON profile contains the same canonical analysis result for scripts and
|
|
215
266
|
future Tabalyst tools.
|
|
216
267
|
|
|
@@ -238,10 +289,17 @@ sample = tabalyst.sample_csv(
|
|
|
238
289
|
rows=1000,
|
|
239
290
|
seed=42,
|
|
240
291
|
)
|
|
292
|
+
|
|
293
|
+
scan = tabalyst.scan("orders.json")
|
|
294
|
+
scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
|
|
295
|
+
reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
241
296
|
```
|
|
242
297
|
|
|
243
298
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
244
299
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
300
|
+
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
301
|
+
writes one `.scan.json` document per source; `from_scan=True` builds reports
|
|
302
|
+
from such documents.
|
|
245
303
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
246
304
|
|
|
247
305
|
Existing artifacts are never replaced silently. Pass `force=True` when
|
|
@@ -253,15 +311,21 @@ Configuration files are strict JSON. A minimal file is:
|
|
|
253
311
|
|
|
254
312
|
```json
|
|
255
313
|
{
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
|
|
314
|
+
"scan": {
|
|
315
|
+
"csv": {
|
|
316
|
+
"delimiter": ";",
|
|
317
|
+
"encoding": "cp1252"
|
|
318
|
+
},
|
|
319
|
+
"values": {"null_markers": ["N/A"]}
|
|
259
320
|
}
|
|
260
321
|
}
|
|
261
322
|
```
|
|
262
323
|
|
|
263
|
-
|
|
264
|
-
|
|
324
|
+
Analysis settings, CSV reading included, go in the `scan` object, shared by
|
|
325
|
+
`tabalyst report` and `tabalyst scan`. Top-level settings shape the report
|
|
326
|
+
presentation, and `csv` configures `tabalyst sample`. Explicit CLI or Python
|
|
327
|
+
arguments override the configuration file, which overrides Tabalyst defaults.
|
|
328
|
+
No configuration file is loaded unless it is passed with `--config`. See the
|
|
265
329
|
[configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
|
|
266
330
|
for all analysis settings.
|
|
267
331
|
|
|
@@ -271,10 +335,11 @@ Tabalyst performs analysis locally and adds no telemetry or remote processing.
|
|
|
271
335
|
Generated JSON and HTML may contain source values and should be shared
|
|
272
336
|
accordingly.
|
|
273
337
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
338
|
+
Reports, samples, and scans read files as a stream, with memory bounded by
|
|
339
|
+
configurable limits, including duplicate-row detection
|
|
340
|
+
(`scan.limits.max_tracked_records`). Reports and scans show the share of the file read;
|
|
341
|
+
throughput estimates, recursive directory input, and parallel batch execution
|
|
342
|
+
will require later work.
|
|
278
343
|
|
|
279
344
|
## Examples and development
|
|
280
345
|
|
|
@@ -31,6 +31,8 @@ changes often: update it before each new test.
|
|
|
31
31
|
| One file, custom name | `tabalyst report data.csv -o report.html` | The file `report.html` |
|
|
32
32
|
| Several files, automatic names | `tabalyst report *.csv` | Beside each source |
|
|
33
33
|
| Several files, one directory | `tabalyst report *.csv -d reports/` | The directory `reports/` |
|
|
34
|
+
| One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
|
|
35
|
+
| From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
|
|
34
36
|
|
|
35
37
|
The simplest command keeps the source filename:
|
|
36
38
|
|
|
@@ -53,6 +55,18 @@ Use `-o` to choose a different HTML filename for one source:
|
|
|
53
55
|
tabalyst report customers.csv -o customer-analysis.html
|
|
54
56
|
```
|
|
55
57
|
|
|
58
|
+
### JSON files
|
|
59
|
+
|
|
60
|
+
A JSON file gets a report too, named `<stem>.report.html` so its profile
|
|
61
|
+
`<stem>.report.json` never replaces the source. Each collection of records,
|
|
62
|
+
such as the `customers` array of `{"customers": [...]}`, is a dataset of the
|
|
63
|
+
report, and nested fields are columns named by their path, such as
|
|
64
|
+
`address.city`:
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
tabalyst report orders.json
|
|
68
|
+
```
|
|
69
|
+
|
|
56
70
|
### Multiple files
|
|
57
71
|
|
|
58
72
|
Report several CSV files at once:
|
|
@@ -167,16 +181,53 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
|
|
|
167
181
|
only the requested sample, plus stratum counts, in memory. Existing outputs
|
|
168
182
|
require `--force`, and an input file is never overwritten.
|
|
169
183
|
|
|
184
|
+
## Tabalyst Scan
|
|
185
|
+
|
|
186
|
+
Describe every field of a CSV or JSON file in one JSON document:
|
|
187
|
+
|
|
188
|
+
```console
|
|
189
|
+
tabalyst scan customers.csv
|
|
190
|
+
tabalyst scan orders.json --collection "$.customers[]"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The default output is `customers.scan.json` beside the source; `-o`, `-d` and
|
|
194
|
+
`--force` work as for the other commands. Tabalyst Scan reads the file once as
|
|
195
|
+
a stream, with memory bounded by configurable limits, and records for each
|
|
196
|
+
field its presence, native types, missing values, frequencies, exact
|
|
197
|
+
statistics, normalization variants, technical type and the result of every
|
|
198
|
+
detector: numbers with decimal commas, dates, booleans, enumerations, email
|
|
199
|
+
addresses, URLs, phone numbers, postal codes, currency amounts, percentages,
|
|
200
|
+
quantities, UUIDs and IP addresses. Values of sensitive fields, such as email
|
|
201
|
+
addresses, are masked by default. Results are written atomically: an
|
|
202
|
+
interrupted scan never leaves a partial file.
|
|
203
|
+
|
|
204
|
+
Build the report from a scan document without reading the source again:
|
|
205
|
+
|
|
206
|
+
```console
|
|
207
|
+
tabalyst report --scan customers.scan.json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
211
|
+
settings differ from the `scan` settings of `--config`.
|
|
212
|
+
|
|
213
|
+
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
214
|
+
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
215
|
+
|
|
170
216
|
## What the report analyzes
|
|
171
217
|
|
|
172
218
|
- Dataset dimensions, missing cells, duplicates, and quality observations.
|
|
173
219
|
- Physical and semantic types with confidence and error rates.
|
|
174
220
|
- Numeric, date, string-length, normalization, and value distributions.
|
|
175
|
-
- Distinct values, representative examples,
|
|
221
|
+
- Distinct values, representative examples, date formats, and semantic types
|
|
222
|
+
such as enumerations, email addresses, phone numbers, and postal codes.
|
|
176
223
|
- CSV record widths, quoting, encoding, and delimiter configuration.
|
|
177
224
|
- A bounded raw-data preview while every record is analyzed.
|
|
178
225
|
|
|
179
|
-
The
|
|
226
|
+
The report is built on Tabalyst Scan: it reads the CSV once as a stream and
|
|
227
|
+
masks values of sensitive columns, such as email addresses, by default.
|
|
228
|
+
Ambiguous dates stay ambiguous; the report shows the evidence of the column
|
|
229
|
+
without applying it. The HTML report is self-contained and works without a CDN
|
|
230
|
+
or network connection.
|
|
180
231
|
The JSON profile contains the same canonical analysis result for scripts and
|
|
181
232
|
future Tabalyst tools.
|
|
182
233
|
|
|
@@ -204,10 +255,17 @@ sample = tabalyst.sample_csv(
|
|
|
204
255
|
rows=1000,
|
|
205
256
|
seed=42,
|
|
206
257
|
)
|
|
258
|
+
|
|
259
|
+
scan = tabalyst.scan("orders.json")
|
|
260
|
+
scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
|
|
261
|
+
reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
207
262
|
```
|
|
208
263
|
|
|
209
264
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
210
265
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
266
|
+
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
267
|
+
writes one `.scan.json` document per source; `from_scan=True` builds reports
|
|
268
|
+
from such documents.
|
|
211
269
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
212
270
|
|
|
213
271
|
Existing artifacts are never replaced silently. Pass `force=True` when
|
|
@@ -219,15 +277,21 @@ Configuration files are strict JSON. A minimal file is:
|
|
|
219
277
|
|
|
220
278
|
```json
|
|
221
279
|
{
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
|
|
280
|
+
"scan": {
|
|
281
|
+
"csv": {
|
|
282
|
+
"delimiter": ";",
|
|
283
|
+
"encoding": "cp1252"
|
|
284
|
+
},
|
|
285
|
+
"values": {"null_markers": ["N/A"]}
|
|
225
286
|
}
|
|
226
287
|
}
|
|
227
288
|
```
|
|
228
289
|
|
|
229
|
-
|
|
230
|
-
|
|
290
|
+
Analysis settings, CSV reading included, go in the `scan` object, shared by
|
|
291
|
+
`tabalyst report` and `tabalyst scan`. Top-level settings shape the report
|
|
292
|
+
presentation, and `csv` configures `tabalyst sample`. Explicit CLI or Python
|
|
293
|
+
arguments override the configuration file, which overrides Tabalyst defaults.
|
|
294
|
+
No configuration file is loaded unless it is passed with `--config`. See the
|
|
231
295
|
[configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
|
|
232
296
|
for all analysis settings.
|
|
233
297
|
|
|
@@ -237,10 +301,11 @@ Tabalyst performs analysis locally and adds no telemetry or remote processing.
|
|
|
237
301
|
Generated JSON and HTML may contain source values and should be shared
|
|
238
302
|
accordingly.
|
|
239
303
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
304
|
+
Reports, samples, and scans read files as a stream, with memory bounded by
|
|
305
|
+
configurable limits, including duplicate-row detection
|
|
306
|
+
(`scan.limits.max_tracked_records`). Reports and scans show the share of the file read;
|
|
307
|
+
throughput estimates, recursive directory input, and parallel batch execution
|
|
308
|
+
will require later work.
|
|
244
309
|
|
|
245
310
|
## Examples and development
|
|
246
311
|
|
|
@@ -8,13 +8,18 @@ examples/
|
|
|
8
8
|
|-- config.json
|
|
9
9
|
|-- input/
|
|
10
10
|
| |-- basic.csv
|
|
11
|
-
|
|
|
11
|
+
| |-- insurance-customers.csv
|
|
12
|
+
| `-- orders.json
|
|
12
13
|
`-- output/
|
|
13
14
|
|-- basic/
|
|
14
15
|
| |-- report.html
|
|
15
16
|
| |-- report.json
|
|
16
17
|
| `-- executions.json
|
|
17
|
-
|
|
18
|
+
|-- insurance-customers/
|
|
19
|
+
| |-- report.html
|
|
20
|
+
| |-- report.json
|
|
21
|
+
| `-- executions.json
|
|
22
|
+
`-- orders/
|
|
18
23
|
|-- report.html
|
|
19
24
|
|-- report.json
|
|
20
25
|
`-- executions.json
|
|
@@ -23,13 +28,16 @@ examples/
|
|
|
23
28
|
`basic.csv` is a five-row smoke example. `insurance-customers.csv` contains 3,000
|
|
24
29
|
synthetic customer and contract records across 34 columns. Its people, contact
|
|
25
30
|
details, and contracts are fictional; email addresses use reserved `.example`
|
|
26
|
-
domains.
|
|
31
|
+
domains. `orders.json` is a synthetic shop export: 60 customers with nested
|
|
32
|
+
addresses, tags and orders, whose report has two datasets, `$` and
|
|
33
|
+
`$.customers[]`.
|
|
27
34
|
|
|
28
|
-
From the repository root, regenerate
|
|
35
|
+
From the repository root, regenerate the outputs with:
|
|
29
36
|
|
|
30
37
|
```console
|
|
31
38
|
tabalyst report examples/input/basic.csv -o examples/output/basic/report.html --config examples/config.json --force
|
|
32
39
|
tabalyst report examples/input/insurance-customers.csv -o examples/output/insurance-customers/report.html --config examples/config.json --force
|
|
40
|
+
tabalyst report examples/input/orders.json -o examples/output/orders/report.html --config examples/config.json --force
|
|
33
41
|
```
|
|
34
42
|
|
|
35
43
|
Every output folder is independent and contains its own HTML report, canonical
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"string_analysis": {
|
|
3
|
+
"very_short_max_length": 5,
|
|
4
|
+
"short_max_length": 20,
|
|
5
|
+
"medium_max_length": 50,
|
|
6
|
+
"long_max_length": 255,
|
|
7
|
+
"length_distribution_max_length": 50,
|
|
8
|
+
"examples_per_length": 10
|
|
9
|
+
},
|
|
10
|
+
"value_examples": {
|
|
11
|
+
"full_distribution_max_distinct": 50,
|
|
12
|
+
"short_text_max_length": 20,
|
|
13
|
+
"short_text_percentile": 0.95,
|
|
14
|
+
"short_text_result_size": 20,
|
|
15
|
+
"long_text_result_size": 20,
|
|
16
|
+
"long_text_truncate_at": 30,
|
|
17
|
+
"truncation_suffix": "...",
|
|
18
|
+
"inline_display_size": 3
|
|
19
|
+
},
|
|
20
|
+
"scan": {
|
|
21
|
+
"csv": {
|
|
22
|
+
"encoding": "utf-8-sig",
|
|
23
|
+
"delimiter": ","
|
|
24
|
+
},
|
|
25
|
+
"values": {
|
|
26
|
+
"null_markers": ["N/A", "NULL"],
|
|
27
|
+
"missing": ["absent", "null", "empty", "blank", "marker"]
|
|
28
|
+
},
|
|
29
|
+
"normalization": {
|
|
30
|
+
"trim": true,
|
|
31
|
+
"collapse_whitespace": true
|
|
32
|
+
},
|
|
33
|
+
"types": {
|
|
34
|
+
"minimum_confidence": 0.95
|
|
35
|
+
},
|
|
36
|
+
"detectors": {
|
|
37
|
+
"date": {
|
|
38
|
+
"orders": ["YMD", "MDY", "DMY"],
|
|
39
|
+
"separators": ["-", "/", "."],
|
|
40
|
+
"ambiguous_order": null
|
|
41
|
+
},
|
|
42
|
+
"enumeration": {
|
|
43
|
+
"minimum_values": 500,
|
|
44
|
+
"maximum_distinct": 49,
|
|
45
|
+
"case_sensitive": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"limits": {
|
|
49
|
+
"max_samples": 100
|
|
50
|
+
},
|
|
51
|
+
"records": {
|
|
52
|
+
"preview": 20,
|
|
53
|
+
"duplicates": true
|
|
54
|
+
},
|
|
55
|
+
"exposure": {
|
|
56
|
+
"sensitive_values": "mask"
|
|
57
|
+
},
|
|
58
|
+
"random_seed": 42
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tabalyst"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Open-source toolkit for understanding and working with unfamiliar data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -34,9 +34,9 @@ classifiers = [
|
|
|
34
34
|
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
|
+
"ijson>=3.3",
|
|
37
38
|
"jinja2>=3.1",
|
|
38
|
-
"
|
|
39
|
-
"pydantic>=2.10",
|
|
39
|
+
"pydantic>=2.11",
|
|
40
40
|
"typer>=0.12",
|
|
41
41
|
]
|
|
42
42
|
|
|
@@ -65,6 +65,9 @@ Issues = "https://github.com/loribel-labs/tabalyst/issues"
|
|
|
65
65
|
[tool.pytest.ini_options]
|
|
66
66
|
testpaths = ["tests"]
|
|
67
67
|
pythonpath = ["src"]
|
|
68
|
+
markers = [
|
|
69
|
+
"lot(name): Tabalyst Scan implementation lot that provides the tested behavior",
|
|
70
|
+
]
|
|
68
71
|
|
|
69
72
|
[tool.ruff]
|
|
70
73
|
line-length = 88
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
"""Public Python interface for Tabalyst."""
|
|
2
2
|
|
|
3
3
|
from tabalyst._version import __version__
|
|
4
|
-
from tabalyst.analysis import analyze_column as analyze_column
|
|
5
|
-
from tabalyst.config import AnalysisConfig as AnalysisConfig
|
|
6
4
|
from tabalyst.errors import ConfigurationError, InputError, ReportError, TabalystError
|
|
5
|
+
from tabalyst.report_config import ReportConfig as ReportConfig
|
|
7
6
|
from tabalyst.report_service import generate_reports as generate_reports
|
|
8
7
|
from tabalyst.reporting import render_report as render_report
|
|
9
8
|
from tabalyst.sampling import SampleMethod as SampleMethod
|
|
10
9
|
from tabalyst.sampling import SampleResult as SampleResult
|
|
11
10
|
from tabalyst.sampling import sample_csv as sample_csv
|
|
12
11
|
from tabalyst.sampling_service import generate_samples as generate_samples
|
|
12
|
+
from tabalyst.scan_service import generate_scans as generate_scans
|
|
13
|
+
from tabalyst.scanner import ScanConfig as ScanConfig
|
|
14
|
+
from tabalyst.scanner import ScanResult as ScanResult
|
|
15
|
+
from tabalyst.scanner import scan as scan
|
|
13
16
|
from tabalyst.service import analyze
|
|
14
17
|
from tabalyst.service import analyze_csv as analyze_csv
|
|
18
|
+
from tabalyst.service import analyze_scan as analyze_scan
|
|
15
19
|
|
|
16
20
|
__all__ = [
|
|
17
21
|
"ConfigurationError",
|
|
18
22
|
"InputError",
|
|
19
23
|
"ReportError",
|
|
24
|
+
"ScanConfig",
|
|
25
|
+
"ScanResult",
|
|
20
26
|
"TabalystError",
|
|
21
27
|
"__version__",
|
|
22
28
|
"analyze",
|
|
23
29
|
"generate_reports",
|
|
24
30
|
"generate_samples",
|
|
31
|
+
"generate_scans",
|
|
25
32
|
"sample_csv",
|
|
33
|
+
"scan",
|
|
26
34
|
]
|
|
27
|
-
|
|
28
|
-
# Alpha compatibility imports remain available, but are not part of the 0.1.0 API.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Batch planning shared by the commands: inputs, outputs and safe writing.
|
|
2
|
+
|
|
3
|
+
Every command resolves its inputs, maps each one to its outputs and rejects the
|
|
4
|
+
whole batch before any work when the plan is unsafe: output name collisions,
|
|
5
|
+
outputs that would replace an input, or existing outputs without ``force``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import glob
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from tabalyst.errors import ConfigurationError, InputError, ReportError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def path_key(path: Path) -> str:
|
|
18
|
+
"""Identity of a path for comparisons, whatever its spelling and case."""
|
|
19
|
+
return os.path.normcase(str(path.resolve()))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_input_specs(input_specs: Sequence[str | Path]) -> list[Path]:
|
|
23
|
+
"""Resolve explicit files and shell-independent, non-recursive glob patterns."""
|
|
24
|
+
if not input_specs:
|
|
25
|
+
raise ConfigurationError("At least one input file is required.")
|
|
26
|
+
|
|
27
|
+
sources: list[Path] = []
|
|
28
|
+
known: set[str] = set()
|
|
29
|
+
for value in input_specs:
|
|
30
|
+
text = os.fspath(value)
|
|
31
|
+
if "**" in text:
|
|
32
|
+
raise ConfigurationError(
|
|
33
|
+
f"Recursive input patterns are not supported yet: {text}"
|
|
34
|
+
)
|
|
35
|
+
if glob.has_magic(text):
|
|
36
|
+
matches = sorted(
|
|
37
|
+
(Path(match) for match in glob.glob(text) if Path(match).is_file()),
|
|
38
|
+
key=path_key,
|
|
39
|
+
)
|
|
40
|
+
if not matches:
|
|
41
|
+
raise InputError(f'Input pattern matched no files: "{text}"')
|
|
42
|
+
else:
|
|
43
|
+
path = Path(text)
|
|
44
|
+
if not path.exists():
|
|
45
|
+
raise InputError(f"Input file does not exist: {path}")
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
raise InputError(f"Input path is not a file: {path}")
|
|
48
|
+
matches = [path]
|
|
49
|
+
|
|
50
|
+
for path in matches:
|
|
51
|
+
key = path_key(path)
|
|
52
|
+
if key not in known:
|
|
53
|
+
sources.append(path)
|
|
54
|
+
known.add(key)
|
|
55
|
+
return sources
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def plan_outputs(
|
|
59
|
+
input_specs: Sequence[str | Path],
|
|
60
|
+
*,
|
|
61
|
+
output: str | Path | None,
|
|
62
|
+
output_dir: str | Path | None,
|
|
63
|
+
output_name: Callable[[Path], str],
|
|
64
|
+
) -> list[tuple[Path, Path]]:
|
|
65
|
+
"""Resolve the inputs and pair each one with its main output.
|
|
66
|
+
|
|
67
|
+
``output`` names the output of a single input; otherwise each output is
|
|
68
|
+
named ``output_name(source)``, in ``output_dir`` or beside its source.
|
|
69
|
+
"""
|
|
70
|
+
if output is not None and output_dir is not None:
|
|
71
|
+
raise ConfigurationError("--output and --output-dir cannot be used together.")
|
|
72
|
+
sources = resolve_input_specs(input_specs)
|
|
73
|
+
if output is not None and len(sources) != 1:
|
|
74
|
+
raise ConfigurationError("--output can only be used with one input file.")
|
|
75
|
+
|
|
76
|
+
destination = Path(output_dir) if output_dir is not None else None
|
|
77
|
+
if destination is not None and destination.exists() and not destination.is_dir():
|
|
78
|
+
raise ReportError(f"Output directory path is a file: {destination}")
|
|
79
|
+
return [
|
|
80
|
+
(
|
|
81
|
+
source,
|
|
82
|
+
Path(output)
|
|
83
|
+
if output is not None
|
|
84
|
+
else (destination if destination is not None else source.parent)
|
|
85
|
+
/ output_name(source),
|
|
86
|
+
)
|
|
87
|
+
for source in sources
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_outputs(
|
|
92
|
+
artifacts: Iterable[tuple[Path, Path]],
|
|
93
|
+
*,
|
|
94
|
+
sources: Iterable[Path],
|
|
95
|
+
force: bool,
|
|
96
|
+
label: str,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Reject unsafe ``(source, output)`` pairs of a whole batch.
|
|
99
|
+
|
|
100
|
+
``label`` names the outputs in messages, such as ``"Report"``.
|
|
101
|
+
"""
|
|
102
|
+
input_keys = {path_key(source) for source in sources}
|
|
103
|
+
owners: dict[str, Path] = {}
|
|
104
|
+
existing: list[Path] = []
|
|
105
|
+
for source, artifact in artifacts:
|
|
106
|
+
if artifact.exists() and artifact.is_dir():
|
|
107
|
+
raise InputError(f"{label} path is a directory: {artifact}")
|
|
108
|
+
key = path_key(artifact)
|
|
109
|
+
if key in input_keys:
|
|
110
|
+
raise InputError(f"{label} would overwrite an input file: {artifact}")
|
|
111
|
+
previous = owners.get(key)
|
|
112
|
+
if previous is not None:
|
|
113
|
+
raise ReportError(
|
|
114
|
+
"Output name collision: "
|
|
115
|
+
f"{previous} and {source} both map to {artifact}."
|
|
116
|
+
)
|
|
117
|
+
owners[key] = source
|
|
118
|
+
if not force and artifact.exists():
|
|
119
|
+
existing.append(artifact)
|
|
120
|
+
if existing:
|
|
121
|
+
lines = "\n".join(f" {path}" for path in existing)
|
|
122
|
+
raise ReportError(
|
|
123
|
+
f"{label} output already exists. Use --force to replace it:\n" + lines
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def apply_default_file_mode(path: Path) -> None:
|
|
128
|
+
"""Give ``path`` the permissions of a plain write under the current umask.
|
|
129
|
+
|
|
130
|
+
Temporary files are created for their owner only, and ``os.replace`` keeps
|
|
131
|
+
that mode on the final file.
|
|
132
|
+
"""
|
|
133
|
+
umask = os.umask(0)
|
|
134
|
+
os.umask(umask)
|
|
135
|
+
os.chmod(path, 0o666 & ~umask)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def write_text_atomic(path: Path, content: str) -> None:
|
|
139
|
+
"""Write through a temporary file in the target directory, then replace.
|
|
140
|
+
|
|
141
|
+
An interrupted write never leaves a partial file at ``path``.
|
|
142
|
+
"""
|
|
143
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
descriptor, name = tempfile.mkstemp(
|
|
145
|
+
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
|
|
146
|
+
)
|
|
147
|
+
temporary = Path(name)
|
|
148
|
+
try:
|
|
149
|
+
apply_default_file_mode(temporary)
|
|
150
|
+
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
|
|
151
|
+
handle.write(content)
|
|
152
|
+
handle.flush()
|
|
153
|
+
os.fsync(handle.fileno())
|
|
154
|
+
os.replace(temporary, path)
|
|
155
|
+
except BaseException:
|
|
156
|
+
temporary.unlink(missing_ok=True)
|
|
157
|
+
raise
|
|
@@ -7,6 +7,7 @@ import typer
|
|
|
7
7
|
from tabalyst import __version__
|
|
8
8
|
from tabalyst.cli.report import report_command
|
|
9
9
|
from tabalyst.cli.sample import sample_command
|
|
10
|
+
from tabalyst.cli.scan import scan_command
|
|
10
11
|
|
|
11
12
|
app = typer.Typer(
|
|
12
13
|
add_completion=False,
|
|
@@ -39,6 +40,7 @@ def root(
|
|
|
39
40
|
|
|
40
41
|
app.command("report")(report_command)
|
|
41
42
|
app.command("sample")(sample_command)
|
|
43
|
+
app.command("scan")(scan_command)
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
def main() -> None:
|