tabalyst 0.4.1__tar.gz → 0.4.3__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.1/src/tabalyst.egg-info → tabalyst-0.4.3}/PKG-INFO +39 -10
- {tabalyst-0.4.1 → tabalyst-0.4.3}/README.md +35 -8
- {tabalyst-0.4.1 → tabalyst-0.4.3}/pyproject.toml +4 -2
- tabalyst-0.4.3/src/tabalyst/cache_service.py +114 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/app.py +2 -0
- tabalyst-0.4.3/src/tabalyst/cli/cache.py +64 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/report.py +14 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/scan.py +15 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/models.py +1 -1
- tabalyst-0.4.3/src/tabalyst/project_open_service.py +179 -0
- tabalyst-0.4.3/src/tabalyst/project_scan_service.py +204 -0
- tabalyst-0.4.3/src/tabalyst/projects/__init__.py +48 -0
- tabalyst-0.4.3/src/tabalyst/projects/_bounded_values.py +449 -0
- tabalyst-0.4.3/src/tabalyst/projects/_cache.py +370 -0
- tabalyst-0.4.3/src/tabalyst/projects/_codec.py +116 -0
- tabalyst-0.4.3/src/tabalyst/projects/_generation.py +118 -0
- tabalyst-0.4.3/src/tabalyst/projects/_locks.py +144 -0
- tabalyst-0.4.3/src/tabalyst/projects/_publication.py +278 -0
- tabalyst-0.4.3/src/tabalyst/projects/_queries.py +481 -0
- tabalyst-0.4.3/src/tabalyst/projects/_query_budget.py +83 -0
- tabalyst-0.4.3/src/tabalyst/projects/_recovery.py +59 -0
- tabalyst-0.4.3/src/tabalyst/projects/_retention.py +70 -0
- tabalyst-0.4.3/src/tabalyst/projects/_schema.py +115 -0
- tabalyst-0.4.3/src/tabalyst/projects/_session.py +226 -0
- tabalyst-0.4.3/src/tabalyst/projects/_staging.py +439 -0
- tabalyst-0.4.3/src/tabalyst/projects/_sync.py +42 -0
- tabalyst-0.4.3/src/tabalyst/projects/_validation.py +459 -0
- tabalyst-0.4.3/src/tabalyst/projects/catalog.py +75 -0
- tabalyst-0.4.3/src/tabalyst/projects/freshness.py +54 -0
- tabalyst-0.4.3/src/tabalyst/projects/identity.py +42 -0
- tabalyst-0.4.3/src/tabalyst/projects/index.py +175 -0
- tabalyst-0.4.3/src/tabalyst/projects/location.py +118 -0
- tabalyst-0.4.3/src/tabalyst/projects/models.py +110 -0
- tabalyst-0.4.3/src/tabalyst/projects/store.py +117 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/report_service.py +21 -2
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/reporting.py +14 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scan_reuse.py +51 -16
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scan_service.py +68 -24
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/api.py +35 -4
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/config.py +3 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/base.py +23 -4
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/boolean.py +9 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/currency.py +10 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/email.py +5 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/enumeration.py +12 -2
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/ip.py +12 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/number.py +23 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/pattern.py +7 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/percentage.py +5 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/phone.py +12 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/postal.py +5 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/quantity.py +5 -1
- tabalyst-0.4.3/src/tabalyst/scanner/detectors/registry.py +832 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/temporal.py +18 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/url.py +8 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/uuid.py +5 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/engine.py +22 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/exposure.py +18 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/field.py +53 -13
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/measures.py +139 -109
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/models.py +7 -4
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/normalization.py +43 -9
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/observations.py +41 -6
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/csv_reader.py +24 -14
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/json_reader.py +32 -4
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/records.py +87 -22
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/structure.py +77 -6
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/technical.py +15 -22
- tabalyst-0.4.3/src/tabalyst/scanner/values.py +1022 -0
- tabalyst-0.4.3/src/tabalyst/scanner/workers.py +391 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/service.py +39 -6
- tabalyst-0.4.3/src/tabalyst/shared_scan_service.py +149 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/static/report.js +35 -24
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/static/theme.css +10 -4
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/templates/report.html +14 -8
- {tabalyst-0.4.1 → tabalyst-0.4.3/src/tabalyst.egg-info}/PKG-INFO +39 -10
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/SOURCES.txt +45 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/requires.txt +2 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_cli.py +7 -2
- tabalyst-0.4.3/tests/test_project_cache.py +439 -0
- tabalyst-0.4.3/tests/test_project_database.py +525 -0
- tabalyst-0.4.3/tests/test_project_generations.py +481 -0
- tabalyst-0.4.3/tests/test_project_identity.py +55 -0
- tabalyst-0.4.3/tests/test_project_index.py +172 -0
- tabalyst-0.4.3/tests/test_project_location.py +89 -0
- tabalyst-0.4.3/tests/test_project_open_service.py +300 -0
- tabalyst-0.4.3/tests/test_project_queries.py +718 -0
- tabalyst-0.4.3/tests/test_project_rescan.py +536 -0
- tabalyst-0.4.3/tests/test_project_scan.py +238 -0
- tabalyst-0.4.3/tests/test_project_sessions.py +441 -0
- tabalyst-0.4.3/tests/test_project_store.py +238 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_public_api.py +12 -15
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_reporting.py +19 -1
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scan_command.py +97 -16
- tabalyst-0.4.3/tests/test_scanner_batches.py +248 -0
- tabalyst-0.4.3/tests/test_shared_scan_cli.py +130 -0
- tabalyst-0.4.3/tests/test_source_freshness.py +79 -0
- tabalyst-0.4.1/src/tabalyst/scanner/detectors/registry.py +0 -594
- tabalyst-0.4.1/src/tabalyst/scanner/values.py +0 -627
- {tabalyst-0.4.1 → tabalyst-0.4.3}/LICENSE +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/MANIFEST.in +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/README.md +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/config.json +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/input/basic.csv +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/setup.cfg +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/__init__.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/batch.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/sample.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/terminal.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/config.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/execution_log.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/progress.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/report_config.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/report_profile.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/sampling.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/sampling_service.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/__init__.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/__init__.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/amount.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/names.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/shape.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/diagnostics.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/paths.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/__init__.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/base.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/top_level.txt +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_sampling.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_amounts.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_config.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_csv.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_email_url.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_exposure.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_paths.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_phone.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_postal.py +0 -0
- {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_uuid_ip.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.3
|
|
4
4
|
Summary: Open-source toolkit for understanding and working with unfamiliar data.
|
|
5
5
|
Author: Gregory Borelli
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/loribel-labs/tabalyst
|
|
|
8
8
|
Project-URL: Repository, https://github.com/loribel-labs/tabalyst.git
|
|
9
9
|
Project-URL: Issues, https://github.com/loribel-labs/tabalyst/issues
|
|
10
10
|
Keywords: csv,data-analysis,data-profiling,data-quality,eda,html-report,json
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
@@ -21,8 +21,10 @@ 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: duckdb==1.5.5
|
|
24
25
|
Requires-Dist: ijson>=3.3
|
|
25
26
|
Requires-Dist: jinja2>=3.1
|
|
27
|
+
Requires-Dist: platformdirs>=4
|
|
26
28
|
Requires-Dist: pydantic>=2.11
|
|
27
29
|
Requires-Dist: typer>=0.12
|
|
28
30
|
Provides-Extra: dev
|
|
@@ -42,8 +44,10 @@ with structured data.
|
|
|
42
44
|
Its first tool is **Tabalyst Report**. The current CSV implementation,
|
|
43
45
|
**Tabalyst CSV Report**, analyzes a CSV file and produces both a structured JSON
|
|
44
46
|
profile and a self-contained interactive HTML report.
|
|
47
|
+
**Tabalyst Scan** describes CSV and JSON files in a scan document, and
|
|
48
|
+
**Tabalyst Sample** creates smaller CSV files.
|
|
45
49
|
|
|
46
|
-
Tabalyst is in
|
|
50
|
+
Tabalyst is in beta. Its interfaces may still change while
|
|
47
51
|
the shared toolkit architecture is being established.
|
|
48
52
|
|
|
49
53
|
## Install
|
|
@@ -54,7 +58,7 @@ Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
|
|
|
54
58
|
pip install --upgrade tabalyst
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
The same command installs Tabalyst or updates it. Tabalyst is in
|
|
61
|
+
The same command installs Tabalyst or updates it. Tabalyst is in beta and
|
|
58
62
|
changes often: update it before each new test.
|
|
59
63
|
|
|
60
64
|
## Tabalyst Report
|
|
@@ -181,6 +185,7 @@ tabalyst report data.csv --delimiter ";"
|
|
|
181
185
|
tabalyst report data.csv --encoding cp1252
|
|
182
186
|
tabalyst report data.csv --config tabalyst.json
|
|
183
187
|
tabalyst report data.csv --verbose
|
|
188
|
+
tabalyst report data.csv --workers 4
|
|
184
189
|
tabalyst report --help
|
|
185
190
|
tabalyst --version
|
|
186
191
|
```
|
|
@@ -224,8 +229,10 @@ tabalyst scan customers.csv
|
|
|
224
229
|
tabalyst scan orders.json --collection "$.customers[]"
|
|
225
230
|
```
|
|
226
231
|
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
For a CSV, the default scan is the project's generation-bound `scan.json` under
|
|
233
|
+
Tabalyst's local storage directory (or `TABALYST_HOME`). The project also holds
|
|
234
|
+
the database and disposable `cache/` used by future tools. `-o` or `-d` writes
|
|
235
|
+
a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
|
|
229
236
|
a stream, with memory bounded by configurable limits, and records for each
|
|
230
237
|
field its presence, native types, missing values, frequencies, exact
|
|
231
238
|
statistics, normalization variants, technical type and the result of every
|
|
@@ -233,9 +240,18 @@ detector: numbers with decimal commas, dates, booleans, enumerations, email
|
|
|
233
240
|
addresses, URLs, phone numbers, postal codes, currency amounts, percentages,
|
|
234
241
|
quantities, UUIDs and IP addresses. Values of sensitive fields, such as email
|
|
235
242
|
addresses, are masked by default. Results are written atomically: an
|
|
236
|
-
interrupted scan never leaves a partial file.
|
|
243
|
+
interrupted scan never leaves a partial file. Files of 16 MiB or more are
|
|
244
|
+
analyzed by several worker processes, with the same result; `--workers`
|
|
245
|
+
chooses their number, `--workers 1` keeps one process.
|
|
237
246
|
|
|
238
|
-
|
|
247
|
+
`tabalyst report customers.csv` reuses the verified project scan when the CSV
|
|
248
|
+
content and requested scan settings are current. It creates a project scan when
|
|
249
|
+
none exists. If the CSV changed, it publishes a new generation, removes the old
|
|
250
|
+
generation and its owned cache when no reader holds it, then builds the report.
|
|
251
|
+
The source check uses size, modification time and SHA-256. An old generation
|
|
252
|
+
held by a reader is retained with a warning.
|
|
253
|
+
|
|
254
|
+
Build the report from a standalone scan document without reading the source again:
|
|
239
255
|
|
|
240
256
|
```console
|
|
241
257
|
tabalyst report --scan customers.scan.json
|
|
@@ -244,6 +260,18 @@ tabalyst report --scan customers.scan.json
|
|
|
244
260
|
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
245
261
|
settings differ from the `scan` settings of `--config`.
|
|
246
262
|
|
|
263
|
+
Inspect or clean disposable project query caches for every project, or for one
|
|
264
|
+
CSV file:
|
|
265
|
+
|
|
266
|
+
```console
|
|
267
|
+
tabalyst cache info
|
|
268
|
+
tabalyst cache info customers.csv
|
|
269
|
+
tabalyst cache clean
|
|
270
|
+
tabalyst cache clean customers.csv
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`cache clean` leaves project scans and databases in place.
|
|
274
|
+
|
|
247
275
|
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
248
276
|
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
249
277
|
|
|
@@ -298,8 +326,9 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
|
298
326
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
299
327
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
300
328
|
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
301
|
-
writes one `.scan.json` document per source
|
|
302
|
-
|
|
329
|
+
writes one standalone `.scan.json` document per source by default. Pass
|
|
330
|
+
`project_storage=True` to use project storage for CSV files. `from_scan=True`
|
|
331
|
+
builds reports from standalone scan documents.
|
|
303
332
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
304
333
|
|
|
305
334
|
Existing artifacts are never replaced silently. Pass `force=True` when
|
|
@@ -8,8 +8,10 @@ with structured data.
|
|
|
8
8
|
Its first tool is **Tabalyst Report**. The current CSV implementation,
|
|
9
9
|
**Tabalyst CSV Report**, analyzes a CSV file and produces both a structured JSON
|
|
10
10
|
profile and a self-contained interactive HTML report.
|
|
11
|
+
**Tabalyst Scan** describes CSV and JSON files in a scan document, and
|
|
12
|
+
**Tabalyst Sample** creates smaller CSV files.
|
|
11
13
|
|
|
12
|
-
Tabalyst is in
|
|
14
|
+
Tabalyst is in beta. Its interfaces may still change while
|
|
13
15
|
the shared toolkit architecture is being established.
|
|
14
16
|
|
|
15
17
|
## Install
|
|
@@ -20,7 +22,7 @@ Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
|
|
|
20
22
|
pip install --upgrade tabalyst
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
The same command installs Tabalyst or updates it. Tabalyst is in
|
|
25
|
+
The same command installs Tabalyst or updates it. Tabalyst is in beta and
|
|
24
26
|
changes often: update it before each new test.
|
|
25
27
|
|
|
26
28
|
## Tabalyst Report
|
|
@@ -147,6 +149,7 @@ tabalyst report data.csv --delimiter ";"
|
|
|
147
149
|
tabalyst report data.csv --encoding cp1252
|
|
148
150
|
tabalyst report data.csv --config tabalyst.json
|
|
149
151
|
tabalyst report data.csv --verbose
|
|
152
|
+
tabalyst report data.csv --workers 4
|
|
150
153
|
tabalyst report --help
|
|
151
154
|
tabalyst --version
|
|
152
155
|
```
|
|
@@ -190,8 +193,10 @@ tabalyst scan customers.csv
|
|
|
190
193
|
tabalyst scan orders.json --collection "$.customers[]"
|
|
191
194
|
```
|
|
192
195
|
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
For a CSV, the default scan is the project's generation-bound `scan.json` under
|
|
197
|
+
Tabalyst's local storage directory (or `TABALYST_HOME`). The project also holds
|
|
198
|
+
the database and disposable `cache/` used by future tools. `-o` or `-d` writes
|
|
199
|
+
a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
|
|
195
200
|
a stream, with memory bounded by configurable limits, and records for each
|
|
196
201
|
field its presence, native types, missing values, frequencies, exact
|
|
197
202
|
statistics, normalization variants, technical type and the result of every
|
|
@@ -199,9 +204,18 @@ detector: numbers with decimal commas, dates, booleans, enumerations, email
|
|
|
199
204
|
addresses, URLs, phone numbers, postal codes, currency amounts, percentages,
|
|
200
205
|
quantities, UUIDs and IP addresses. Values of sensitive fields, such as email
|
|
201
206
|
addresses, are masked by default. Results are written atomically: an
|
|
202
|
-
interrupted scan never leaves a partial file.
|
|
207
|
+
interrupted scan never leaves a partial file. Files of 16 MiB or more are
|
|
208
|
+
analyzed by several worker processes, with the same result; `--workers`
|
|
209
|
+
chooses their number, `--workers 1` keeps one process.
|
|
203
210
|
|
|
204
|
-
|
|
211
|
+
`tabalyst report customers.csv` reuses the verified project scan when the CSV
|
|
212
|
+
content and requested scan settings are current. It creates a project scan when
|
|
213
|
+
none exists. If the CSV changed, it publishes a new generation, removes the old
|
|
214
|
+
generation and its owned cache when no reader holds it, then builds the report.
|
|
215
|
+
The source check uses size, modification time and SHA-256. An old generation
|
|
216
|
+
held by a reader is retained with a warning.
|
|
217
|
+
|
|
218
|
+
Build the report from a standalone scan document without reading the source again:
|
|
205
219
|
|
|
206
220
|
```console
|
|
207
221
|
tabalyst report --scan customers.scan.json
|
|
@@ -210,6 +224,18 @@ tabalyst report --scan customers.scan.json
|
|
|
210
224
|
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
211
225
|
settings differ from the `scan` settings of `--config`.
|
|
212
226
|
|
|
227
|
+
Inspect or clean disposable project query caches for every project, or for one
|
|
228
|
+
CSV file:
|
|
229
|
+
|
|
230
|
+
```console
|
|
231
|
+
tabalyst cache info
|
|
232
|
+
tabalyst cache info customers.csv
|
|
233
|
+
tabalyst cache clean
|
|
234
|
+
tabalyst cache clean customers.csv
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`cache clean` leaves project scans and databases in place.
|
|
238
|
+
|
|
213
239
|
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
214
240
|
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
215
241
|
|
|
@@ -264,8 +290,9 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
|
264
290
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
265
291
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
266
292
|
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
267
|
-
writes one `.scan.json` document per source
|
|
268
|
-
|
|
293
|
+
writes one standalone `.scan.json` document per source by default. Pass
|
|
294
|
+
`project_storage=True` to use project storage for CSV files. `from_scan=True`
|
|
295
|
+
builds reports from standalone scan documents.
|
|
269
296
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
270
297
|
|
|
271
298
|
Existing artifacts are never replaced silently. Pass `force=True` when
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tabalyst"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "Open-source toolkit for understanding and working with unfamiliar data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -22,7 +22,7 @@ keywords = [
|
|
|
22
22
|
"json",
|
|
23
23
|
]
|
|
24
24
|
classifiers = [
|
|
25
|
-
"Development Status ::
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
26
|
"Intended Audience :: Developers",
|
|
27
27
|
"Intended Audience :: Science/Research",
|
|
28
28
|
"Operating System :: OS Independent",
|
|
@@ -34,8 +34,10 @@ classifiers = [
|
|
|
34
34
|
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
|
+
"duckdb==1.5.5",
|
|
37
38
|
"ijson>=3.3",
|
|
38
39
|
"jinja2>=3.1",
|
|
40
|
+
"platformdirs>=4",
|
|
39
41
|
"pydantic>=2.11",
|
|
40
42
|
"typer>=0.12",
|
|
41
43
|
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Public cleanup of disposable project query caches."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tabalyst.errors import InputError
|
|
7
|
+
from tabalyst.projects._cache import clean_query_caches, inventory_query_caches
|
|
8
|
+
from tabalyst.projects._locks import ProjectBusyError
|
|
9
|
+
from tabalyst.projects.identity import is_project_id
|
|
10
|
+
from tabalyst.projects.index import (
|
|
11
|
+
document_key,
|
|
12
|
+
find_project,
|
|
13
|
+
read_index,
|
|
14
|
+
source_key,
|
|
15
|
+
)
|
|
16
|
+
from tabalyst.projects.location import StorageLocation
|
|
17
|
+
from tabalyst.projects.store import read_project
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CacheCleanResult:
|
|
22
|
+
projects: int
|
|
23
|
+
removed: int
|
|
24
|
+
removed_bytes: int
|
|
25
|
+
skipped: tuple[str, ...]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CacheInfo:
|
|
30
|
+
projects: int
|
|
31
|
+
directories: int
|
|
32
|
+
logical_bytes: int
|
|
33
|
+
skipped: tuple[str, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _selected_projects(
|
|
37
|
+
source: str | Path | None = None,
|
|
38
|
+
*,
|
|
39
|
+
location: StorageLocation | None = None,
|
|
40
|
+
) -> tuple[StorageLocation, list]:
|
|
41
|
+
location = location or StorageLocation.local()
|
|
42
|
+
if source is None:
|
|
43
|
+
try:
|
|
44
|
+
directories = sorted(location.projects_dir.iterdir())
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
directories = []
|
|
47
|
+
projects = []
|
|
48
|
+
for directory in directories:
|
|
49
|
+
if is_project_id(directory.name):
|
|
50
|
+
try:
|
|
51
|
+
projects.append(read_project(location, directory.name))
|
|
52
|
+
except InputError:
|
|
53
|
+
continue
|
|
54
|
+
else:
|
|
55
|
+
key = source_key(Path(source))
|
|
56
|
+
index = read_index(location)
|
|
57
|
+
project_id = index.projects.get(key) if index else None
|
|
58
|
+
try:
|
|
59
|
+
selected = read_project(location, project_id) if project_id else None
|
|
60
|
+
except InputError:
|
|
61
|
+
selected = None
|
|
62
|
+
if selected is None or document_key(selected) != key:
|
|
63
|
+
selected = find_project(location, Path(source))
|
|
64
|
+
if selected is None:
|
|
65
|
+
raise InputError(f"No Tabalyst project for source: {source}")
|
|
66
|
+
projects = [selected]
|
|
67
|
+
return location, projects
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cache_info(
|
|
71
|
+
source: str | Path | None = None,
|
|
72
|
+
*,
|
|
73
|
+
location: StorageLocation | None = None,
|
|
74
|
+
) -> CacheInfo:
|
|
75
|
+
"""Summarize owned disposable cache files without modifying them."""
|
|
76
|
+
location, projects = _selected_projects(source, location=location)
|
|
77
|
+
directories = 0
|
|
78
|
+
logical_bytes = 0
|
|
79
|
+
skipped = []
|
|
80
|
+
for project in projects:
|
|
81
|
+
inventory = inventory_query_caches(location, project.project_id)
|
|
82
|
+
for entry in inventory.entries:
|
|
83
|
+
if entry.status == "candidate":
|
|
84
|
+
directories += 1
|
|
85
|
+
logical_bytes += entry.logical_bytes
|
|
86
|
+
else:
|
|
87
|
+
skipped.append(f"{project.project_id}/{entry.path}: {entry.reason}")
|
|
88
|
+
return CacheInfo(len(projects), directories, logical_bytes, tuple(skipped))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def clean_cache(
|
|
92
|
+
source: str | Path | None = None,
|
|
93
|
+
*,
|
|
94
|
+
location: StorageLocation | None = None,
|
|
95
|
+
) -> CacheCleanResult:
|
|
96
|
+
"""Clean owned disposable cache files for one source or all projects."""
|
|
97
|
+
location, projects = _selected_projects(source, location=location)
|
|
98
|
+
removed = 0
|
|
99
|
+
removed_bytes = 0
|
|
100
|
+
skipped = []
|
|
101
|
+
for project in projects:
|
|
102
|
+
plan = inventory_query_caches(location, project.project_id)
|
|
103
|
+
try:
|
|
104
|
+
result = clean_query_caches(location, project.project_id, plan=plan)
|
|
105
|
+
except ProjectBusyError as exc:
|
|
106
|
+
skipped.append(f"{project.project_id}/cache: {exc}")
|
|
107
|
+
continue
|
|
108
|
+
for entry in result.entries:
|
|
109
|
+
if entry.status == "removed":
|
|
110
|
+
removed += 1
|
|
111
|
+
removed_bytes += entry.removed_bytes
|
|
112
|
+
elif entry.status in ("skipped", "error"):
|
|
113
|
+
skipped.append(f"{project.project_id}/{entry.path}: {entry.reason}")
|
|
114
|
+
return CacheCleanResult(len(projects), removed, removed_bytes, tuple(skipped))
|
|
@@ -5,6 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from tabalyst import __version__
|
|
8
|
+
from tabalyst.cli.cache import cache_app
|
|
8
9
|
from tabalyst.cli.report import report_command
|
|
9
10
|
from tabalyst.cli.sample import sample_command
|
|
10
11
|
from tabalyst.cli.scan import scan_command
|
|
@@ -41,6 +42,7 @@ def root(
|
|
|
41
42
|
app.command("report")(report_command)
|
|
42
43
|
app.command("sample")(sample_command)
|
|
43
44
|
app.command("scan")(scan_command)
|
|
45
|
+
app.add_typer(cache_app, name="cache")
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
def main() -> None:
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""CLI adapter for disposable project cache cleanup."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from tabalyst.cache_service import cache_info, clean_cache
|
|
9
|
+
from tabalyst.cli.terminal import error_exit_code
|
|
10
|
+
from tabalyst.errors import TabalystError
|
|
11
|
+
|
|
12
|
+
cache_app = typer.Typer(help="Manage disposable project caches.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _count(value: int, singular: str) -> str:
|
|
16
|
+
if value == 1:
|
|
17
|
+
return f"{value} {singular}"
|
|
18
|
+
plural = singular.removesuffix("y") + "ies" if singular.endswith("y") else singular + "s"
|
|
19
|
+
return f"{value} {plural}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cache_app.command("info")
|
|
23
|
+
def info_command(
|
|
24
|
+
source: Annotated[
|
|
25
|
+
Path | None,
|
|
26
|
+
typer.Argument(help="CSV file whose project cache should be inspected."),
|
|
27
|
+
] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Show the size and directory count of all caches or one source cache."""
|
|
30
|
+
try:
|
|
31
|
+
result = cache_info(source)
|
|
32
|
+
except TabalystError as exc:
|
|
33
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
34
|
+
raise typer.Exit(error_exit_code(exc)) from exc
|
|
35
|
+
typer.echo(
|
|
36
|
+
f"Cache: {_count(result.directories, 'directory')} in "
|
|
37
|
+
f"{_count(result.projects, 'project')} "
|
|
38
|
+
f"({result.logical_bytes:,} bytes)."
|
|
39
|
+
)
|
|
40
|
+
for warning in result.skipped:
|
|
41
|
+
typer.echo(f"Retained: {warning}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cache_app.command("clean")
|
|
45
|
+
def clean_command(
|
|
46
|
+
source: Annotated[
|
|
47
|
+
Path | None,
|
|
48
|
+
typer.Argument(help="CSV file whose project cache should be cleaned."),
|
|
49
|
+
] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Clean all project caches, or the cache for one source file."""
|
|
52
|
+
try:
|
|
53
|
+
result = clean_cache(source)
|
|
54
|
+
except TabalystError as exc:
|
|
55
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
56
|
+
raise typer.Exit(error_exit_code(exc)) from exc
|
|
57
|
+
typer.echo(
|
|
58
|
+
f"Cleaned {_count(result.removed, 'cache directory')} in "
|
|
59
|
+
f"{_count(result.projects, 'project')} "
|
|
60
|
+
f"({result.removed_bytes:,} bytes).",
|
|
61
|
+
err=True,
|
|
62
|
+
)
|
|
63
|
+
for warning in result.skipped:
|
|
64
|
+
typer.echo(f"Warning: retained {warning}", err=True)
|
|
@@ -58,6 +58,8 @@ def _print_batch_result(
|
|
|
58
58
|
typer.echo(" " + " | ".join(details), err=True)
|
|
59
59
|
|
|
60
60
|
for success in batch.successes:
|
|
61
|
+
for warning in success.warnings:
|
|
62
|
+
typer.echo(f"Warning [{success.job.source}]: {warning}", err=True)
|
|
61
63
|
if success.source_checked is False:
|
|
62
64
|
typer.echo(
|
|
63
65
|
f"Warning [{success.job.source}]: source "
|
|
@@ -143,6 +145,17 @@ def report_command(
|
|
|
143
145
|
help="Disable interactive file and phase progress.",
|
|
144
146
|
),
|
|
145
147
|
] = False,
|
|
148
|
+
workers: Annotated[
|
|
149
|
+
int | None,
|
|
150
|
+
typer.Option(
|
|
151
|
+
"--workers",
|
|
152
|
+
min=1,
|
|
153
|
+
help=(
|
|
154
|
+
"Processes that analyze values: 1 for one process. By default, "
|
|
155
|
+
"files of 16 MiB or more use one per spare processor."
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
] = None,
|
|
146
159
|
) -> None:
|
|
147
160
|
"""Analyze and profile datasets with Tabalyst Report."""
|
|
148
161
|
if quiet and verbose:
|
|
@@ -162,6 +175,7 @@ def report_command(
|
|
|
162
175
|
force=force,
|
|
163
176
|
on_progress=progress,
|
|
164
177
|
from_scan=from_scan,
|
|
178
|
+
workers=workers,
|
|
165
179
|
)
|
|
166
180
|
except TabalystError as exc:
|
|
167
181
|
typer.echo(f"Error: {exc}", err=True)
|
|
@@ -76,6 +76,8 @@ def _print_batch_result(
|
|
|
76
76
|
for success in batch.successes:
|
|
77
77
|
if success.result.status == "partial":
|
|
78
78
|
_print_partial(success)
|
|
79
|
+
for warning in success.warnings:
|
|
80
|
+
typer.echo(f"Warning [{success.job.source}]: {warning}", err=True)
|
|
79
81
|
for failure in batch.failures:
|
|
80
82
|
typer.echo(f"Error [{failure.job.source}]: {failure.error}", err=True)
|
|
81
83
|
|
|
@@ -149,6 +151,17 @@ def scan_command(
|
|
|
149
151
|
bool,
|
|
150
152
|
typer.Option("--no-progress", help="Disable interactive progress."),
|
|
151
153
|
] = False,
|
|
154
|
+
workers: Annotated[
|
|
155
|
+
int | None,
|
|
156
|
+
typer.Option(
|
|
157
|
+
"--workers",
|
|
158
|
+
min=1,
|
|
159
|
+
help=(
|
|
160
|
+
"Processes that analyze values: 1 for one process. By default, "
|
|
161
|
+
"files of 16 MiB or more use one per spare processor."
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
] = None,
|
|
152
165
|
) -> None:
|
|
153
166
|
"""Scan CSV and JSON files into a complete JSON description of their data."""
|
|
154
167
|
if quiet and verbose:
|
|
@@ -168,6 +181,8 @@ def scan_command(
|
|
|
168
181
|
collections=collection,
|
|
169
182
|
force=force,
|
|
170
183
|
on_progress=progress,
|
|
184
|
+
workers=workers,
|
|
185
|
+
project_storage=True,
|
|
171
186
|
)
|
|
172
187
|
except TabalystError as exc:
|
|
173
188
|
typer.echo(f"Error: {exc}", err=True)
|
|
@@ -378,7 +378,7 @@ class DatasetProfile(ResultModel):
|
|
|
378
378
|
|
|
379
379
|
class ReportProfile(ResultModel):
|
|
380
380
|
format_version: Literal["0.1.0a"] = "0.1.0a"
|
|
381
|
-
format_revision: Literal[
|
|
381
|
+
format_revision: Literal[8] = 8
|
|
382
382
|
generated_at: datetime
|
|
383
383
|
processing_seconds: FiniteFloat
|
|
384
384
|
source: SourceInfo
|