tabalyst 0.4.3__tar.gz → 0.4.4__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.4.3/src/tabalyst.egg-info → tabalyst-0.4.4}/PKG-INFO +32 -16
- {tabalyst-0.4.3 → tabalyst-0.4.4}/README.md +31 -15
- {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/README.md +4 -2
- {tabalyst-0.4.3 → tabalyst-0.4.4}/pyproject.toml +1 -1
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/report.py +8 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/models.py +41 -3
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/location.py +7 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/report_profile.py +34 -2
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/report_service.py +4 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/reporting.py +52 -2
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/service.py +71 -2
- tabalyst-0.4.4/src/tabalyst/shared_scan_service.py +79 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/static/theme.css +17 -0
- tabalyst-0.4.4/src/tabalyst/templates/column.html +66 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/templates/report.html +2 -2
- {tabalyst-0.4.3 → tabalyst-0.4.4/src/tabalyst.egg-info}/PKG-INFO +32 -16
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/SOURCES.txt +1 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_cli.py +2 -2
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_public_api.py +22 -0
- tabalyst-0.4.4/tests/test_reporting.py +296 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scan_command.py +5 -12
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_shared_scan_cli.py +33 -24
- tabalyst-0.4.3/src/tabalyst/shared_scan_service.py +0 -149
- tabalyst-0.4.3/tests/test_reporting.py +0 -137
- {tabalyst-0.4.3 → tabalyst-0.4.4}/LICENSE +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/MANIFEST.in +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/config.json +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/input/basic.csv +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/setup.cfg +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/__init__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/batch.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cache_service.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/app.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/cache.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/sample.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/scan.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/terminal.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/config.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/execution_log.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/progress.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/project_open_service.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/project_scan_service.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/__init__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_bounded_values.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_cache.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_codec.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_generation.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_locks.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_publication.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_queries.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_query_budget.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_recovery.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_retention.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_schema.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_session.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_staging.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_sync.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_validation.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/catalog.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/freshness.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/identity.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/index.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/models.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/store.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/report_config.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/sampling.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/sampling_service.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scan_reuse.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scan_service.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/__init__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/api.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/config.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/__init__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/amount.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/base.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/boolean.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/currency.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/email.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/ip.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/names.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/number.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/pattern.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/percentage.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/phone.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/postal.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/quantity.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/registry.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/shape.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/temporal.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/url.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/uuid.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/diagnostics.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/engine.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/exposure.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/field.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/measures.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/models.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/normalization.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/observations.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/paths.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/__init__.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/base.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/json_reader.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/records.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/structure.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/technical.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/values.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/workers.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/static/report.js +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/requires.txt +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/top_level.txt +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_cache.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_database.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_generations.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_identity.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_index.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_location.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_open_service.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_queries.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_rescan.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_scan.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_sessions.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_store.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_sampling.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_amounts.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_batches.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_config.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_csv.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_email_url.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_exposure.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_paths.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_phone.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_postal.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_uuid_ip.py +0 -0
- {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_source_freshness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabalyst
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Open-source toolkit for understanding and working with unfamiliar data.
|
|
5
5
|
Author: Gregory Borelli
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,6 +87,20 @@ customers.json
|
|
|
87
87
|
executions.json
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
Add `--details` to generate one self-contained HTML page per column:
|
|
91
|
+
|
|
92
|
+
```console
|
|
93
|
+
tabalyst report customers.csv --details
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The **Columns** table then links to those pages. Each page has its own sidebar
|
|
97
|
+
and shows values, counts, detected formats and other analysis directly. The
|
|
98
|
+
subfolder takes the HTML filename without `.html`; for `-o report.html`, pages
|
|
99
|
+
are in `report/`. Names use `col-01-` and a shortened slug of the column name,
|
|
100
|
+
in source order. Details are off by default. `--no-details` states that choice
|
|
101
|
+
explicitly; with `--force`, it removes prior generated column pages while
|
|
102
|
+
preserving other files in the folder.
|
|
103
|
+
|
|
90
104
|
Use `-o` to choose a different HTML filename for one source:
|
|
91
105
|
|
|
92
106
|
```console
|
|
@@ -157,8 +171,10 @@ instead.
|
|
|
157
171
|
|
|
158
172
|
### Safe batch behavior
|
|
159
173
|
|
|
160
|
-
Before processing begins, Tabalyst resolves every input and planned
|
|
161
|
-
stops the entire batch if
|
|
174
|
+
Before processing begins, Tabalyst resolves every input and planned main HTML
|
|
175
|
+
and JSON output. It stops the entire batch if those names collide or if an
|
|
176
|
+
output already exists. With `--details`, each column page is checked after its
|
|
177
|
+
profile has been built, before that report's files are written.
|
|
162
178
|
Use `--force` only when replacing all matching report artifacts is intentional:
|
|
163
179
|
|
|
164
180
|
```console
|
|
@@ -229,9 +245,9 @@ tabalyst scan customers.csv
|
|
|
229
245
|
tabalyst scan orders.json --collection "$.customers[]"
|
|
230
246
|
```
|
|
231
247
|
|
|
232
|
-
For a CSV, the default scan is
|
|
233
|
-
|
|
234
|
-
|
|
248
|
+
For a CSV, the default scan is a reusable `scan.json` under Tabalyst's local
|
|
249
|
+
storage directory (or `TABALYST_HOME`). The default workflow does not build a
|
|
250
|
+
DuckDB database. `-o` or `-d` writes
|
|
235
251
|
a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
|
|
236
252
|
a stream, with memory bounded by configurable limits, and records for each
|
|
237
253
|
field its presence, native types, missing values, frequencies, exact
|
|
@@ -244,12 +260,11 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
|
|
|
244
260
|
analyzed by several worker processes, with the same result; `--workers`
|
|
245
261
|
chooses their number, `--workers 1` keeps one process.
|
|
246
262
|
|
|
247
|
-
`tabalyst report customers.csv` reuses the verified
|
|
248
|
-
content and requested scan settings are current. It creates a
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
held by a reader is retained with a warning.
|
|
263
|
+
`tabalyst report customers.csv` reuses the verified stored scan when the CSV
|
|
264
|
+
content and requested scan settings are current. It creates a scan when none
|
|
265
|
+
exists and atomically replaces a stale one. The source check uses size,
|
|
266
|
+
modification time and SHA-256. Existing DuckDB projects from 0.4.3 are left
|
|
267
|
+
untouched.
|
|
253
268
|
|
|
254
269
|
Build the report from a standalone scan document without reading the source again:
|
|
255
270
|
|
|
@@ -260,8 +275,8 @@ tabalyst report --scan customers.scan.json
|
|
|
260
275
|
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
261
276
|
settings differ from the `scan` settings of `--config`.
|
|
262
277
|
|
|
263
|
-
Inspect or clean disposable
|
|
264
|
-
CSV file:
|
|
278
|
+
Inspect or clean disposable query caches of existing DuckDB projects, for every
|
|
279
|
+
project or for one CSV file:
|
|
265
280
|
|
|
266
281
|
```console
|
|
267
282
|
tabalyst cache info
|
|
@@ -270,7 +285,8 @@ tabalyst cache clean
|
|
|
270
285
|
tabalyst cache clean customers.csv
|
|
271
286
|
```
|
|
272
287
|
|
|
273
|
-
`cache clean` leaves project scans and databases in place.
|
|
288
|
+
`cache clean` leaves project scans and databases in place. Ordinary scans and
|
|
289
|
+
reports do not create these query caches.
|
|
274
290
|
|
|
275
291
|
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
276
292
|
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
@@ -327,7 +343,7 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
|
327
343
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
328
344
|
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
329
345
|
writes one standalone `.scan.json` document per source by default. Pass
|
|
330
|
-
`project_storage=True` to use
|
|
346
|
+
`project_storage=True` to use the command's scan-only storage for CSV files. `from_scan=True`
|
|
331
347
|
builds reports from standalone scan documents.
|
|
332
348
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
333
349
|
|
|
@@ -51,6 +51,20 @@ customers.json
|
|
|
51
51
|
executions.json
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
Add `--details` to generate one self-contained HTML page per column:
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
tabalyst report customers.csv --details
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The **Columns** table then links to those pages. Each page has its own sidebar
|
|
61
|
+
and shows values, counts, detected formats and other analysis directly. The
|
|
62
|
+
subfolder takes the HTML filename without `.html`; for `-o report.html`, pages
|
|
63
|
+
are in `report/`. Names use `col-01-` and a shortened slug of the column name,
|
|
64
|
+
in source order. Details are off by default. `--no-details` states that choice
|
|
65
|
+
explicitly; with `--force`, it removes prior generated column pages while
|
|
66
|
+
preserving other files in the folder.
|
|
67
|
+
|
|
54
68
|
Use `-o` to choose a different HTML filename for one source:
|
|
55
69
|
|
|
56
70
|
```console
|
|
@@ -121,8 +135,10 @@ instead.
|
|
|
121
135
|
|
|
122
136
|
### Safe batch behavior
|
|
123
137
|
|
|
124
|
-
Before processing begins, Tabalyst resolves every input and planned
|
|
125
|
-
stops the entire batch if
|
|
138
|
+
Before processing begins, Tabalyst resolves every input and planned main HTML
|
|
139
|
+
and JSON output. It stops the entire batch if those names collide or if an
|
|
140
|
+
output already exists. With `--details`, each column page is checked after its
|
|
141
|
+
profile has been built, before that report's files are written.
|
|
126
142
|
Use `--force` only when replacing all matching report artifacts is intentional:
|
|
127
143
|
|
|
128
144
|
```console
|
|
@@ -193,9 +209,9 @@ tabalyst scan customers.csv
|
|
|
193
209
|
tabalyst scan orders.json --collection "$.customers[]"
|
|
194
210
|
```
|
|
195
211
|
|
|
196
|
-
For a CSV, the default scan is
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
For a CSV, the default scan is a reusable `scan.json` under Tabalyst's local
|
|
213
|
+
storage directory (or `TABALYST_HOME`). The default workflow does not build a
|
|
214
|
+
DuckDB database. `-o` or `-d` writes
|
|
199
215
|
a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
|
|
200
216
|
a stream, with memory bounded by configurable limits, and records for each
|
|
201
217
|
field its presence, native types, missing values, frequencies, exact
|
|
@@ -208,12 +224,11 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
|
|
|
208
224
|
analyzed by several worker processes, with the same result; `--workers`
|
|
209
225
|
chooses their number, `--workers 1` keeps one process.
|
|
210
226
|
|
|
211
|
-
`tabalyst report customers.csv` reuses the verified
|
|
212
|
-
content and requested scan settings are current. It creates a
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
held by a reader is retained with a warning.
|
|
227
|
+
`tabalyst report customers.csv` reuses the verified stored scan when the CSV
|
|
228
|
+
content and requested scan settings are current. It creates a scan when none
|
|
229
|
+
exists and atomically replaces a stale one. The source check uses size,
|
|
230
|
+
modification time and SHA-256. Existing DuckDB projects from 0.4.3 are left
|
|
231
|
+
untouched.
|
|
217
232
|
|
|
218
233
|
Build the report from a standalone scan document without reading the source again:
|
|
219
234
|
|
|
@@ -224,8 +239,8 @@ tabalyst report --scan customers.scan.json
|
|
|
224
239
|
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
225
240
|
settings differ from the `scan` settings of `--config`.
|
|
226
241
|
|
|
227
|
-
Inspect or clean disposable
|
|
228
|
-
CSV file:
|
|
242
|
+
Inspect or clean disposable query caches of existing DuckDB projects, for every
|
|
243
|
+
project or for one CSV file:
|
|
229
244
|
|
|
230
245
|
```console
|
|
231
246
|
tabalyst cache info
|
|
@@ -234,7 +249,8 @@ tabalyst cache clean
|
|
|
234
249
|
tabalyst cache clean customers.csv
|
|
235
250
|
```
|
|
236
251
|
|
|
237
|
-
`cache clean` leaves project scans and databases in place.
|
|
252
|
+
`cache clean` leaves project scans and databases in place. Ordinary scans and
|
|
253
|
+
reports do not create these query caches.
|
|
238
254
|
|
|
239
255
|
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
240
256
|
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
@@ -291,7 +307,7 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
|
291
307
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
292
308
|
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
293
309
|
writes one standalone `.scan.json` document per source by default. Pass
|
|
294
|
-
`project_storage=True` to use
|
|
310
|
+
`project_storage=True` to use the command's scan-only storage for CSV files. `from_scan=True`
|
|
295
311
|
builds reports from standalone scan documents.
|
|
296
312
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
297
313
|
|
|
@@ -18,6 +18,7 @@ examples/
|
|
|
18
18
|
|-- insurance-customers/
|
|
19
19
|
| |-- report.html
|
|
20
20
|
| |-- report.json
|
|
21
|
+
| |-- report/ (34 standalone column pages with --details)
|
|
21
22
|
| `-- executions.json
|
|
22
23
|
`-- orders/
|
|
23
24
|
|-- report.html
|
|
@@ -36,12 +37,13 @@ From the repository root, regenerate the outputs with:
|
|
|
36
37
|
|
|
37
38
|
```console
|
|
38
39
|
tabalyst report examples/input/basic.csv -o examples/output/basic/report.html --config examples/config.json --force
|
|
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/insurance-customers.csv -o examples/output/insurance-customers/report.html --config examples/config.json --details --force
|
|
40
41
|
tabalyst report examples/input/orders.json -o examples/output/orders/report.html --config examples/config.json --force
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
Every output folder is independent and contains its own HTML report, canonical
|
|
44
|
-
JSON profile
|
|
45
|
+
JSON profile and cumulative `executions.json` history. The insurance example
|
|
46
|
+
also has standalone HTML pages under `report/`. Delete an output folder
|
|
45
47
|
before regenerating when a fresh one-entry execution history is required.
|
|
46
48
|
|
|
47
49
|
The input datasets and generated output are public repository examples. The wheel
|
|
@@ -130,6 +130,13 @@ def report_command(
|
|
|
130
130
|
bool,
|
|
131
131
|
typer.Option("--force", "-f", help="Replace all existing report artifacts."),
|
|
132
132
|
] = False,
|
|
133
|
+
details: Annotated[
|
|
134
|
+
bool,
|
|
135
|
+
typer.Option(
|
|
136
|
+
"--details/--no-details",
|
|
137
|
+
help="Generate standalone HTML pages for every column (off by default).",
|
|
138
|
+
),
|
|
139
|
+
] = False,
|
|
133
140
|
quiet: Annotated[
|
|
134
141
|
bool,
|
|
135
142
|
typer.Option("--quiet", "-q", help="Suppress success messages."),
|
|
@@ -173,6 +180,7 @@ def report_command(
|
|
|
173
180
|
encoding=encoding,
|
|
174
181
|
config_path=config,
|
|
175
182
|
force=force,
|
|
183
|
+
details=details,
|
|
176
184
|
on_progress=progress,
|
|
177
185
|
from_scan=from_scan,
|
|
178
186
|
workers=workers,
|
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
"""Serializable report profile (revision
|
|
1
|
+
"""Serializable report profile (revision 9), independent from presentation.
|
|
2
2
|
|
|
3
3
|
Built from a Tabalyst Scan result by ``report_profile.py``.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Any, Literal
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, FiniteFloat
|
|
10
10
|
|
|
11
11
|
from tabalyst.report_config import ReportConfig
|
|
12
|
+
from tabalyst.scanner.models import (
|
|
13
|
+
Adaptive,
|
|
14
|
+
BooleanCounts,
|
|
15
|
+
Coverage,
|
|
16
|
+
Evidence,
|
|
17
|
+
Missing,
|
|
18
|
+
Presence,
|
|
19
|
+
StringCategories,
|
|
20
|
+
StringCharacteristics,
|
|
21
|
+
StringLengths,
|
|
22
|
+
TemporalStats,
|
|
23
|
+
ValueAt,
|
|
24
|
+
)
|
|
25
|
+
from tabalyst.scanner.models import NumericStats as ScanNumericStats
|
|
12
26
|
|
|
13
27
|
|
|
14
28
|
class ResultModel(BaseModel):
|
|
@@ -176,6 +190,29 @@ class DetectorProfile(ResultModel):
|
|
|
176
190
|
ambiguous_count: int
|
|
177
191
|
invalid_count: int
|
|
178
192
|
formats: list[DetectorFormat]
|
|
193
|
+
coverage: Coverage | None = None
|
|
194
|
+
evidence: Evidence | None = None
|
|
195
|
+
details: dict[str, Any] = {}
|
|
196
|
+
adaptive: Adaptive | None = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ScanFieldDetails(ResultModel):
|
|
200
|
+
"""Useful bounded field evidence retained from the exposed Scan document."""
|
|
201
|
+
|
|
202
|
+
first_record: int | None
|
|
203
|
+
occurrences: int
|
|
204
|
+
value_count: int
|
|
205
|
+
presence: Presence
|
|
206
|
+
native_types: dict[str, int]
|
|
207
|
+
strings: StringCategories
|
|
208
|
+
missing: Missing
|
|
209
|
+
first: ValueAt | None
|
|
210
|
+
last: ValueAt | None
|
|
211
|
+
string_characteristics: StringCharacteristics
|
|
212
|
+
string_lengths: StringLengths | None
|
|
213
|
+
numeric: ScanNumericStats | None
|
|
214
|
+
booleans: BooleanCounts | None
|
|
215
|
+
temporal: TemporalStats | None
|
|
179
216
|
|
|
180
217
|
|
|
181
218
|
class ColumnProfile(ResultModel):
|
|
@@ -208,6 +245,7 @@ class ColumnProfile(ResultModel):
|
|
|
208
245
|
numeric: NumericStats | None = None
|
|
209
246
|
# Detectors that recognized values, in scan order; failed ones included.
|
|
210
247
|
detectors: list[DetectorProfile] = []
|
|
248
|
+
scan_details: ScanFieldDetails | None = None
|
|
211
249
|
|
|
212
250
|
|
|
213
251
|
class DatasetSummary(ResultModel):
|
|
@@ -378,7 +416,7 @@ class DatasetProfile(ResultModel):
|
|
|
378
416
|
|
|
379
417
|
class ReportProfile(ResultModel):
|
|
380
418
|
format_version: Literal["0.1.0a"] = "0.1.0a"
|
|
381
|
-
format_revision: Literal[
|
|
419
|
+
format_revision: Literal[9] = 9
|
|
382
420
|
generated_at: datetime
|
|
383
421
|
processing_seconds: FiniteFloat
|
|
384
422
|
source: SourceInfo
|
|
@@ -9,6 +9,7 @@ or the directory named by ``TABALYST_HOME``, which tests and a server can use
|
|
|
9
9
|
to relocate storage.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import hashlib
|
|
12
13
|
import os
|
|
13
14
|
import sys
|
|
14
15
|
from dataclasses import dataclass
|
|
@@ -16,6 +17,7 @@ from pathlib import Path
|
|
|
16
17
|
|
|
17
18
|
import platformdirs
|
|
18
19
|
|
|
20
|
+
from tabalyst.batch import path_key
|
|
19
21
|
from tabalyst.errors import ConfigurationError, InputError
|
|
20
22
|
from tabalyst.projects.identity import is_project_id
|
|
21
23
|
|
|
@@ -71,6 +73,11 @@ class StorageLocation:
|
|
|
71
73
|
def projects_dir(self) -> Path:
|
|
72
74
|
return self.workspace_dir / "projects"
|
|
73
75
|
|
|
76
|
+
def shared_scan_path(self, source: Path) -> Path:
|
|
77
|
+
"""The scan-only document for a resolved source, independent of DuckDB projects."""
|
|
78
|
+
key = path_key(source.resolve()).encode("utf-8", "surrogatepass")
|
|
79
|
+
return self.workspace_dir / "scans" / hashlib.sha256(key).hexdigest() / SCAN_FILE_NAME
|
|
80
|
+
|
|
74
81
|
@property
|
|
75
82
|
def workspace_dir(self) -> Path:
|
|
76
83
|
return self.root / "workspaces" / self.workspace_id
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Report profile built from a Tabalyst Scan result (profile revision
|
|
1
|
+
"""Report profile built from a Tabalyst Scan result (profile revision 9).
|
|
2
2
|
|
|
3
3
|
``build_profile`` turns a scan result, fresh or read back from a scan
|
|
4
4
|
document, into the profile that ``reporting.py`` renders: the columns come
|
|
@@ -45,6 +45,7 @@ from tabalyst.models import (
|
|
|
45
45
|
NumericStats,
|
|
46
46
|
PreviewRow,
|
|
47
47
|
ReportProfile,
|
|
48
|
+
ScanFieldDetails,
|
|
48
49
|
SourceInfo,
|
|
49
50
|
StringLengthDistribution,
|
|
50
51
|
StringLengthExample,
|
|
@@ -293,7 +294,10 @@ def build_value_profile(
|
|
|
293
294
|
"""
|
|
294
295
|
settings = config.value_examples
|
|
295
296
|
listed, complete = _listed_values(field)
|
|
296
|
-
if complete and
|
|
297
|
+
if complete and (
|
|
298
|
+
len(listed) <= settings.full_distribution_max_distinct
|
|
299
|
+
or field.interpretations.primary == "enumeration"
|
|
300
|
+
):
|
|
297
301
|
return ValueProfile(
|
|
298
302
|
selection="complete",
|
|
299
303
|
sampled_distinct_count=len(listed),
|
|
@@ -558,6 +562,10 @@ def build_detectors(field: FieldResult) -> list[DetectorProfile]:
|
|
|
558
562
|
)
|
|
559
563
|
for item in detector.formats
|
|
560
564
|
],
|
|
565
|
+
coverage=coverage,
|
|
566
|
+
evidence=detector.evidence,
|
|
567
|
+
details=detector.details,
|
|
568
|
+
adaptive=detector.adaptive,
|
|
561
569
|
)
|
|
562
570
|
)
|
|
563
571
|
return detectors
|
|
@@ -613,6 +621,30 @@ def build_column(
|
|
|
613
621
|
string_profile=build_string_profile(field, inferred, config),
|
|
614
622
|
numeric=build_numeric(field, inferred),
|
|
615
623
|
detectors=build_detectors(field),
|
|
624
|
+
scan_details=ScanFieldDetails(
|
|
625
|
+
first_record=field.first_record,
|
|
626
|
+
occurrences=field.occurrences,
|
|
627
|
+
value_count=field.values.count,
|
|
628
|
+
presence=field.presence,
|
|
629
|
+
native_types=dict(field.native_types),
|
|
630
|
+
strings=field.strings,
|
|
631
|
+
missing=field.missing,
|
|
632
|
+
first=field.values.first,
|
|
633
|
+
last=field.values.last,
|
|
634
|
+
string_characteristics=field.string_characteristics,
|
|
635
|
+
string_lengths=(
|
|
636
|
+
field.string_lengths.value
|
|
637
|
+
if isinstance(field.string_lengths, Complete)
|
|
638
|
+
else None
|
|
639
|
+
),
|
|
640
|
+
numeric=field.numeric.value if isinstance(field.numeric, Complete) else None,
|
|
641
|
+
booleans=(
|
|
642
|
+
field.booleans.value if isinstance(field.booleans, Complete) else None
|
|
643
|
+
),
|
|
644
|
+
temporal=(
|
|
645
|
+
field.temporal.value if isinstance(field.temporal, Complete) else None
|
|
646
|
+
),
|
|
647
|
+
),
|
|
616
648
|
)
|
|
617
649
|
|
|
618
650
|
|
|
@@ -174,6 +174,7 @@ def generate_reports(
|
|
|
174
174
|
encoding: str | None = None,
|
|
175
175
|
config_path: ConfigPath | None = None,
|
|
176
176
|
force: bool = False,
|
|
177
|
+
details: bool = False,
|
|
177
178
|
on_progress: ProgressCallback | None = None,
|
|
178
179
|
from_scan: bool = False,
|
|
179
180
|
workers: int | None = None,
|
|
@@ -184,6 +185,7 @@ def generate_reports(
|
|
|
184
185
|
``tabalyst scan``: each report is built from its document without reading
|
|
185
186
|
the source again, after the staleness checks of ``tabalyst.scan_reuse``.
|
|
186
187
|
Otherwise ``workers`` is passed to ``tabalyst.scan()``.
|
|
188
|
+
Set ``details=True`` to write a standalone HTML page for each column.
|
|
187
189
|
"""
|
|
188
190
|
if from_scan and (separator is not None or encoding is not None):
|
|
189
191
|
raise ConfigurationError(
|
|
@@ -245,6 +247,7 @@ def generate_reports(
|
|
|
245
247
|
scan_layer,
|
|
246
248
|
config_paths,
|
|
247
249
|
force=force,
|
|
250
|
+
details=details,
|
|
248
251
|
on_progress=forward,
|
|
249
252
|
)
|
|
250
253
|
else:
|
|
@@ -254,6 +257,7 @@ def generate_reports(
|
|
|
254
257
|
config,
|
|
255
258
|
config_paths,
|
|
256
259
|
force=force,
|
|
260
|
+
details=details,
|
|
257
261
|
on_progress=forward,
|
|
258
262
|
workers=workers,
|
|
259
263
|
scan_layer=report_scan_layer,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Render self-contained analysis results as an interactive HTML report."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import unicodedata
|
|
4
5
|
from collections import defaultdict
|
|
5
6
|
from importlib.resources import files
|
|
6
7
|
from pathlib import Path
|
|
@@ -9,7 +10,7 @@ from jinja2 import Environment, StrictUndefined
|
|
|
9
10
|
from markupsafe import Markup, escape
|
|
10
11
|
|
|
11
12
|
from tabalyst._version import get_version
|
|
12
|
-
from tabalyst.models import ReportProfile
|
|
13
|
+
from tabalyst.models import ColumnProfile, DatasetProfile, ReportProfile
|
|
13
14
|
|
|
14
15
|
# Plain names of the scan measures a limit can stop (design 8 and 11).
|
|
15
16
|
MEASURE_LABELS = {
|
|
@@ -73,7 +74,24 @@ def load_profile(path: str | Path) -> ReportProfile:
|
|
|
73
74
|
return ReportProfile.model_validate_json(Path(path).read_text(encoding="utf-8"))
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
def
|
|
77
|
+
def column_pages(
|
|
78
|
+
profile: ReportProfile, report_path: Path
|
|
79
|
+
) -> list[tuple[Path, DatasetProfile, ColumnProfile]]:
|
|
80
|
+
"""Name standalone column pages in source order, across all datasets."""
|
|
81
|
+
pages = []
|
|
82
|
+
number = 0
|
|
83
|
+
for dataset in profile.datasets:
|
|
84
|
+
for column in dataset.columns:
|
|
85
|
+
number += 1
|
|
86
|
+
plain = unicodedata.normalize("NFKD", column.name).encode("ascii", "ignore").decode()
|
|
87
|
+
slug = re.sub(r"[^a-z0-9]+", "-", plain.lower()).strip("-")[:48].rstrip("-")
|
|
88
|
+
slug = slug or "unnamed"
|
|
89
|
+
path = report_path.with_suffix("") / f"col-{number:02d}-{slug}.html"
|
|
90
|
+
pages.append((path, dataset, column))
|
|
91
|
+
return pages
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def render_report(profile: ReportProfile, *, column_links: dict[tuple[str, str], str] | None = None) -> str:
|
|
77
95
|
"""Render exclusively from serialized analysis results; CSV access is unnecessary."""
|
|
78
96
|
resources = files("tabalyst")
|
|
79
97
|
environment = Environment(autoescape=True, undefined=StrictUndefined)
|
|
@@ -141,4 +159,36 @@ def render_report(profile: ReportProfile) -> str:
|
|
|
141
159
|
reason_settings=REASON_SETTINGS,
|
|
142
160
|
theme_css=resources.joinpath("static/theme.css").read_text(encoding="utf-8"),
|
|
143
161
|
report_js=resources.joinpath("static/report.js").read_text(encoding="utf-8"),
|
|
162
|
+
column_links=column_links or {},
|
|
144
163
|
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def render_column_report(
|
|
167
|
+
profile: ReportProfile,
|
|
168
|
+
dataset: DatasetProfile,
|
|
169
|
+
column: ColumnProfile,
|
|
170
|
+
report_href: str,
|
|
171
|
+
) -> str:
|
|
172
|
+
"""Render a self-contained page from one serialized column profile."""
|
|
173
|
+
resources = files("tabalyst")
|
|
174
|
+
environment = Environment(autoescape=True, undefined=StrictUndefined)
|
|
175
|
+
environment.filters["count"] = lambda number: f"{number:,}"
|
|
176
|
+
environment.filters["number"] = format_number
|
|
177
|
+
template = environment.from_string(
|
|
178
|
+
resources.joinpath("templates/column.html").read_text(encoding="utf-8")
|
|
179
|
+
)
|
|
180
|
+
rendered = template.render(
|
|
181
|
+
report=profile,
|
|
182
|
+
ds=dataset,
|
|
183
|
+
column=column,
|
|
184
|
+
report_href=report_href,
|
|
185
|
+
tabalyst_version=get_version(),
|
|
186
|
+
limits=[item for item in dataset.limits.measures if item.column_id == column.id],
|
|
187
|
+
issues=[item for item in dataset.issues if column.id in item.column_ids],
|
|
188
|
+
diagnostics=[
|
|
189
|
+
item for item in dataset.limits.diagnostics if item.path == column.path
|
|
190
|
+
],
|
|
191
|
+
theme_css=resources.joinpath("static/theme.css").read_text(encoding="utf-8"),
|
|
192
|
+
report_js=resources.joinpath("static/report.js").read_text(encoding="utf-8"),
|
|
193
|
+
)
|
|
194
|
+
return re.sub(r"(?m)^[ \t]+$", "", rendered)
|