tabalyst 0.4.2__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.
Files changed (143) hide show
  1. {tabalyst-0.4.2/src/tabalyst.egg-info → tabalyst-0.4.4}/PKG-INFO +53 -11
  2. {tabalyst-0.4.2 → tabalyst-0.4.4}/README.md +49 -9
  3. {tabalyst-0.4.2 → tabalyst-0.4.4}/examples/README.md +4 -2
  4. {tabalyst-0.4.2 → tabalyst-0.4.4}/pyproject.toml +4 -2
  5. tabalyst-0.4.4/src/tabalyst/cache_service.py +114 -0
  6. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/cli/app.py +2 -0
  7. tabalyst-0.4.4/src/tabalyst/cli/cache.py +64 -0
  8. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/cli/report.py +10 -0
  9. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/cli/scan.py +3 -0
  10. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/models.py +41 -3
  11. tabalyst-0.4.4/src/tabalyst/project_open_service.py +179 -0
  12. tabalyst-0.4.4/src/tabalyst/project_scan_service.py +204 -0
  13. tabalyst-0.4.4/src/tabalyst/projects/__init__.py +48 -0
  14. tabalyst-0.4.4/src/tabalyst/projects/_bounded_values.py +449 -0
  15. tabalyst-0.4.4/src/tabalyst/projects/_cache.py +370 -0
  16. tabalyst-0.4.4/src/tabalyst/projects/_codec.py +116 -0
  17. tabalyst-0.4.4/src/tabalyst/projects/_generation.py +118 -0
  18. tabalyst-0.4.4/src/tabalyst/projects/_locks.py +144 -0
  19. tabalyst-0.4.4/src/tabalyst/projects/_publication.py +278 -0
  20. tabalyst-0.4.4/src/tabalyst/projects/_queries.py +481 -0
  21. tabalyst-0.4.4/src/tabalyst/projects/_query_budget.py +83 -0
  22. tabalyst-0.4.4/src/tabalyst/projects/_recovery.py +59 -0
  23. tabalyst-0.4.4/src/tabalyst/projects/_retention.py +70 -0
  24. tabalyst-0.4.4/src/tabalyst/projects/_schema.py +115 -0
  25. tabalyst-0.4.4/src/tabalyst/projects/_session.py +226 -0
  26. tabalyst-0.4.4/src/tabalyst/projects/_staging.py +439 -0
  27. tabalyst-0.4.4/src/tabalyst/projects/_sync.py +42 -0
  28. tabalyst-0.4.4/src/tabalyst/projects/_validation.py +459 -0
  29. tabalyst-0.4.4/src/tabalyst/projects/catalog.py +75 -0
  30. tabalyst-0.4.4/src/tabalyst/projects/freshness.py +54 -0
  31. tabalyst-0.4.4/src/tabalyst/projects/identity.py +42 -0
  32. tabalyst-0.4.4/src/tabalyst/projects/index.py +175 -0
  33. tabalyst-0.4.4/src/tabalyst/projects/location.py +125 -0
  34. tabalyst-0.4.4/src/tabalyst/projects/models.py +110 -0
  35. tabalyst-0.4.4/src/tabalyst/projects/store.py +117 -0
  36. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/report_profile.py +34 -2
  37. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/report_service.py +17 -2
  38. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/reporting.py +52 -2
  39. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scan_reuse.py +51 -16
  40. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scan_service.py +65 -25
  41. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/records.py +20 -13
  42. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/service.py +105 -6
  43. tabalyst-0.4.4/src/tabalyst/shared_scan_service.py +79 -0
  44. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/static/report.js +35 -24
  45. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/static/theme.css +23 -1
  46. tabalyst-0.4.4/src/tabalyst/templates/column.html +66 -0
  47. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/templates/report.html +13 -7
  48. {tabalyst-0.4.2 → tabalyst-0.4.4/src/tabalyst.egg-info}/PKG-INFO +53 -11
  49. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst.egg-info/SOURCES.txt +44 -1
  50. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst.egg-info/requires.txt +2 -0
  51. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_cli.py +7 -2
  52. tabalyst-0.4.4/tests/test_project_cache.py +439 -0
  53. tabalyst-0.4.4/tests/test_project_database.py +525 -0
  54. tabalyst-0.4.4/tests/test_project_generations.py +481 -0
  55. tabalyst-0.4.4/tests/test_project_identity.py +55 -0
  56. tabalyst-0.4.4/tests/test_project_index.py +172 -0
  57. tabalyst-0.4.4/tests/test_project_location.py +89 -0
  58. tabalyst-0.4.4/tests/test_project_open_service.py +300 -0
  59. tabalyst-0.4.4/tests/test_project_queries.py +718 -0
  60. tabalyst-0.4.4/tests/test_project_rescan.py +536 -0
  61. tabalyst-0.4.4/tests/test_project_scan.py +238 -0
  62. tabalyst-0.4.4/tests/test_project_sessions.py +441 -0
  63. tabalyst-0.4.4/tests/test_project_store.py +238 -0
  64. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_public_api.py +34 -15
  65. tabalyst-0.4.4/tests/test_reporting.py +296 -0
  66. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scan_command.py +36 -17
  67. tabalyst-0.4.4/tests/test_shared_scan_cli.py +139 -0
  68. tabalyst-0.4.4/tests/test_source_freshness.py +79 -0
  69. tabalyst-0.4.2/tests/test_reporting.py +0 -137
  70. {tabalyst-0.4.2 → tabalyst-0.4.4}/LICENSE +0 -0
  71. {tabalyst-0.4.2 → tabalyst-0.4.4}/MANIFEST.in +0 -0
  72. {tabalyst-0.4.2 → tabalyst-0.4.4}/examples/config.json +0 -0
  73. {tabalyst-0.4.2 → tabalyst-0.4.4}/examples/input/basic.csv +0 -0
  74. {tabalyst-0.4.2 → tabalyst-0.4.4}/examples/input/insurance-customers.csv +0 -0
  75. {tabalyst-0.4.2 → tabalyst-0.4.4}/setup.cfg +0 -0
  76. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/__init__.py +0 -0
  77. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/__main__.py +0 -0
  78. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/_version.py +0 -0
  79. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/batch.py +0 -0
  80. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/cli/__init__.py +0 -0
  81. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/cli/sample.py +0 -0
  82. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/cli/terminal.py +0 -0
  83. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/config.py +0 -0
  84. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/errors.py +0 -0
  85. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/execution_log.py +0 -0
  86. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/progress.py +0 -0
  87. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/report_config.py +0 -0
  88. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/sampling.py +0 -0
  89. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/sampling_service.py +0 -0
  90. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/__init__.py +0 -0
  91. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/api.py +0 -0
  92. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/config.py +0 -0
  93. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/__init__.py +0 -0
  94. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/amount.py +0 -0
  95. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/base.py +0 -0
  96. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/boolean.py +0 -0
  97. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/currency.py +0 -0
  98. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/email.py +0 -0
  99. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
  100. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/ip.py +0 -0
  101. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/names.py +0 -0
  102. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/number.py +0 -0
  103. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/pattern.py +0 -0
  104. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/percentage.py +0 -0
  105. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/phone.py +0 -0
  106. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/postal.py +0 -0
  107. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/quantity.py +0 -0
  108. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/registry.py +0 -0
  109. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/shape.py +0 -0
  110. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/temporal.py +0 -0
  111. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/url.py +0 -0
  112. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/uuid.py +0 -0
  113. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/diagnostics.py +0 -0
  114. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/engine.py +0 -0
  115. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/exposure.py +0 -0
  116. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/field.py +0 -0
  117. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/measures.py +0 -0
  118. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/models.py +0 -0
  119. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/normalization.py +0 -0
  120. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/observations.py +0 -0
  121. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/paths.py +0 -0
  122. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/__init__.py +0 -0
  123. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/base.py +0 -0
  124. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
  125. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/json_reader.py +0 -0
  126. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/structure.py +0 -0
  127. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/technical.py +0 -0
  128. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/values.py +0 -0
  129. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst/scanner/workers.py +0 -0
  130. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  131. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst.egg-info/entry_points.txt +0 -0
  132. {tabalyst-0.4.2 → tabalyst-0.4.4}/src/tabalyst.egg-info/top_level.txt +0 -0
  133. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_sampling.py +0 -0
  134. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_amounts.py +0 -0
  135. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_batches.py +0 -0
  136. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_config.py +0 -0
  137. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_csv.py +0 -0
  138. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_email_url.py +0 -0
  139. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_exposure.py +0 -0
  140. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_paths.py +0 -0
  141. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_phone.py +0 -0
  142. {tabalyst-0.4.2 → tabalyst-0.4.4}/tests/test_scanner_postal.py +0 -0
  143. {tabalyst-0.4.2 → tabalyst-0.4.4}/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.2
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
@@ -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 :: 3 - Alpha
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 active alpha development. Its interfaces may still change while
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 alpha and
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
@@ -83,6 +87,20 @@ customers.json
83
87
  executions.json
84
88
  ```
85
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
+
86
104
  Use `-o` to choose a different HTML filename for one source:
87
105
 
88
106
  ```console
@@ -153,8 +171,10 @@ instead.
153
171
 
154
172
  ### Safe batch behavior
155
173
 
156
- Before processing begins, Tabalyst resolves every input and planned output. It
157
- stops the entire batch if output names collide or if an output already exists.
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.
158
178
  Use `--force` only when replacing all matching report artifacts is intentional:
159
179
 
160
180
  ```console
@@ -225,8 +245,10 @@ tabalyst scan customers.csv
225
245
  tabalyst scan orders.json --collection "$.customers[]"
226
246
  ```
227
247
 
228
- The default output is `customers.scan.json` beside the source; `-o`, `-d` and
229
- `--force` work as for the other commands. Tabalyst Scan reads the file once as
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
251
+ a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
230
252
  a stream, with memory bounded by configurable limits, and records for each
231
253
  field its presence, native types, missing values, frequencies, exact
232
254
  statistics, normalization variants, technical type and the result of every
@@ -238,7 +260,13 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
238
260
  analyzed by several worker processes, with the same result; `--workers`
239
261
  chooses their number, `--workers 1` keeps one process.
240
262
 
241
- Build the report from a scan document without reading the source again:
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.
268
+
269
+ Build the report from a standalone scan document without reading the source again:
242
270
 
243
271
  ```console
244
272
  tabalyst report --scan customers.scan.json
@@ -247,6 +275,19 @@ tabalyst report --scan customers.scan.json
247
275
  Tabalyst refuses a scan whose source changed since it was written, or whose
248
276
  settings differ from the `scan` settings of `--config`.
249
277
 
278
+ Inspect or clean disposable query caches of existing DuckDB projects, for every
279
+ project or for one CSV file:
280
+
281
+ ```console
282
+ tabalyst cache info
283
+ tabalyst cache info customers.csv
284
+ tabalyst cache clean
285
+ tabalyst cache clean customers.csv
286
+ ```
287
+
288
+ `cache clean` leaves project scans and databases in place. Ordinary scans and
289
+ reports do not create these query caches.
290
+
250
291
  See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
251
292
  and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
252
293
 
@@ -301,8 +342,9 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
301
342
  `analyze()` returns the JSON-serializable profile for one report.
302
343
  `generate_reports()` returns the complete batch plan, successes, and failures.
303
344
  `scan()` returns a scan result without writing anything; `generate_scans()`
304
- writes one `.scan.json` document per source; `from_scan=True` builds reports
305
- from such documents.
345
+ writes one standalone `.scan.json` document per source by default. Pass
346
+ `project_storage=True` to use the command's scan-only storage for CSV files. `from_scan=True`
347
+ builds reports from standalone scan documents.
306
348
  Expected failures derive from `tabalyst.TabalystError`.
307
349
 
308
350
  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 active alpha development. Its interfaces may still change while
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 alpha and
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
@@ -49,6 +51,20 @@ customers.json
49
51
  executions.json
50
52
  ```
51
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
+
52
68
  Use `-o` to choose a different HTML filename for one source:
53
69
 
54
70
  ```console
@@ -119,8 +135,10 @@ instead.
119
135
 
120
136
  ### Safe batch behavior
121
137
 
122
- Before processing begins, Tabalyst resolves every input and planned output. It
123
- stops the entire batch if output names collide or if an output already exists.
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.
124
142
  Use `--force` only when replacing all matching report artifacts is intentional:
125
143
 
126
144
  ```console
@@ -191,8 +209,10 @@ tabalyst scan customers.csv
191
209
  tabalyst scan orders.json --collection "$.customers[]"
192
210
  ```
193
211
 
194
- The default output is `customers.scan.json` beside the source; `-o`, `-d` and
195
- `--force` work as for the other commands. Tabalyst Scan reads the file once as
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
215
+ a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
196
216
  a stream, with memory bounded by configurable limits, and records for each
197
217
  field its presence, native types, missing values, frequencies, exact
198
218
  statistics, normalization variants, technical type and the result of every
@@ -204,7 +224,13 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
204
224
  analyzed by several worker processes, with the same result; `--workers`
205
225
  chooses their number, `--workers 1` keeps one process.
206
226
 
207
- Build the report from a scan document without reading the source again:
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.
232
+
233
+ Build the report from a standalone scan document without reading the source again:
208
234
 
209
235
  ```console
210
236
  tabalyst report --scan customers.scan.json
@@ -213,6 +239,19 @@ tabalyst report --scan customers.scan.json
213
239
  Tabalyst refuses a scan whose source changed since it was written, or whose
214
240
  settings differ from the `scan` settings of `--config`.
215
241
 
242
+ Inspect or clean disposable query caches of existing DuckDB projects, for every
243
+ project or for one CSV file:
244
+
245
+ ```console
246
+ tabalyst cache info
247
+ tabalyst cache info customers.csv
248
+ tabalyst cache clean
249
+ tabalyst cache clean customers.csv
250
+ ```
251
+
252
+ `cache clean` leaves project scans and databases in place. Ordinary scans and
253
+ reports do not create these query caches.
254
+
216
255
  See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
217
256
  and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
218
257
 
@@ -267,8 +306,9 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
267
306
  `analyze()` returns the JSON-serializable profile for one report.
268
307
  `generate_reports()` returns the complete batch plan, successes, and failures.
269
308
  `scan()` returns a scan result without writing anything; `generate_scans()`
270
- writes one `.scan.json` document per source; `from_scan=True` builds reports
271
- from such documents.
309
+ writes one standalone `.scan.json` document per source by default. Pass
310
+ `project_storage=True` to use the command's scan-only storage for CSV files. `from_scan=True`
311
+ builds reports from standalone scan documents.
272
312
  Expected failures derive from `tabalyst.TabalystError`.
273
313
 
274
314
  Existing artifacts are never replaced silently. Pass `force=True` when
@@ -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, and cumulative `executions.json` history. Delete an output folder
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabalyst"
7
- version = "0.4.2"
7
+ version = "0.4.4"
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 :: 3 - Alpha",
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 "
@@ -128,6 +130,13 @@ def report_command(
128
130
  bool,
129
131
  typer.Option("--force", "-f", help="Replace all existing report artifacts."),
130
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,
131
140
  quiet: Annotated[
132
141
  bool,
133
142
  typer.Option("--quiet", "-q", help="Suppress success messages."),
@@ -171,6 +180,7 @@ def report_command(
171
180
  encoding=encoding,
172
181
  config_path=config,
173
182
  force=force,
183
+ details=details,
174
184
  on_progress=progress,
175
185
  from_scan=from_scan,
176
186
  workers=workers,
@@ -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
 
@@ -180,6 +182,7 @@ def scan_command(
180
182
  force=force,
181
183
  on_progress=progress,
182
184
  workers=workers,
185
+ project_storage=True,
183
186
  )
184
187
  except TabalystError as exc:
185
188
  typer.echo(f"Error: {exc}", err=True)
@@ -1,14 +1,28 @@
1
- """Serializable report profile (revision 6), independent from presentation.
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[8] = 8
419
+ format_revision: Literal[9] = 9
382
420
  generated_at: datetime
383
421
  processing_seconds: FiniteFloat
384
422
  source: SourceInfo