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.
Files changed (144) hide show
  1. {tabalyst-0.4.3/src/tabalyst.egg-info → tabalyst-0.4.4}/PKG-INFO +32 -16
  2. {tabalyst-0.4.3 → tabalyst-0.4.4}/README.md +31 -15
  3. {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/README.md +4 -2
  4. {tabalyst-0.4.3 → tabalyst-0.4.4}/pyproject.toml +1 -1
  5. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/report.py +8 -0
  6. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/models.py +41 -3
  7. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/location.py +7 -0
  8. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/report_profile.py +34 -2
  9. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/report_service.py +4 -0
  10. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/reporting.py +52 -2
  11. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/service.py +71 -2
  12. tabalyst-0.4.4/src/tabalyst/shared_scan_service.py +79 -0
  13. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/static/theme.css +17 -0
  14. tabalyst-0.4.4/src/tabalyst/templates/column.html +66 -0
  15. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/templates/report.html +2 -2
  16. {tabalyst-0.4.3 → tabalyst-0.4.4/src/tabalyst.egg-info}/PKG-INFO +32 -16
  17. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/SOURCES.txt +1 -0
  18. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_cli.py +2 -2
  19. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_public_api.py +22 -0
  20. tabalyst-0.4.4/tests/test_reporting.py +296 -0
  21. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scan_command.py +5 -12
  22. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_shared_scan_cli.py +33 -24
  23. tabalyst-0.4.3/src/tabalyst/shared_scan_service.py +0 -149
  24. tabalyst-0.4.3/tests/test_reporting.py +0 -137
  25. {tabalyst-0.4.3 → tabalyst-0.4.4}/LICENSE +0 -0
  26. {tabalyst-0.4.3 → tabalyst-0.4.4}/MANIFEST.in +0 -0
  27. {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/config.json +0 -0
  28. {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/input/basic.csv +0 -0
  29. {tabalyst-0.4.3 → tabalyst-0.4.4}/examples/input/insurance-customers.csv +0 -0
  30. {tabalyst-0.4.3 → tabalyst-0.4.4}/setup.cfg +0 -0
  31. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/__init__.py +0 -0
  32. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/__main__.py +0 -0
  33. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/_version.py +0 -0
  34. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/batch.py +0 -0
  35. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cache_service.py +0 -0
  36. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/__init__.py +0 -0
  37. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/app.py +0 -0
  38. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/cache.py +0 -0
  39. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/sample.py +0 -0
  40. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/scan.py +0 -0
  41. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/cli/terminal.py +0 -0
  42. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/config.py +0 -0
  43. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/errors.py +0 -0
  44. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/execution_log.py +0 -0
  45. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/progress.py +0 -0
  46. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/project_open_service.py +0 -0
  47. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/project_scan_service.py +0 -0
  48. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/__init__.py +0 -0
  49. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_bounded_values.py +0 -0
  50. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_cache.py +0 -0
  51. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_codec.py +0 -0
  52. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_generation.py +0 -0
  53. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_locks.py +0 -0
  54. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_publication.py +0 -0
  55. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_queries.py +0 -0
  56. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_query_budget.py +0 -0
  57. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_recovery.py +0 -0
  58. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_retention.py +0 -0
  59. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_schema.py +0 -0
  60. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_session.py +0 -0
  61. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_staging.py +0 -0
  62. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_sync.py +0 -0
  63. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/_validation.py +0 -0
  64. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/catalog.py +0 -0
  65. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/freshness.py +0 -0
  66. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/identity.py +0 -0
  67. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/index.py +0 -0
  68. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/models.py +0 -0
  69. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/projects/store.py +0 -0
  70. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/report_config.py +0 -0
  71. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/sampling.py +0 -0
  72. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/sampling_service.py +0 -0
  73. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scan_reuse.py +0 -0
  74. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scan_service.py +0 -0
  75. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/__init__.py +0 -0
  76. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/api.py +0 -0
  77. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/config.py +0 -0
  78. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/__init__.py +0 -0
  79. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/amount.py +0 -0
  80. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/base.py +0 -0
  81. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/boolean.py +0 -0
  82. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/currency.py +0 -0
  83. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/email.py +0 -0
  84. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
  85. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/ip.py +0 -0
  86. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/names.py +0 -0
  87. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/number.py +0 -0
  88. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/pattern.py +0 -0
  89. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/percentage.py +0 -0
  90. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/phone.py +0 -0
  91. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/postal.py +0 -0
  92. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/quantity.py +0 -0
  93. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/registry.py +0 -0
  94. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/shape.py +0 -0
  95. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/temporal.py +0 -0
  96. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/url.py +0 -0
  97. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/detectors/uuid.py +0 -0
  98. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/diagnostics.py +0 -0
  99. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/engine.py +0 -0
  100. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/exposure.py +0 -0
  101. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/field.py +0 -0
  102. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/measures.py +0 -0
  103. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/models.py +0 -0
  104. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/normalization.py +0 -0
  105. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/observations.py +0 -0
  106. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/paths.py +0 -0
  107. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/__init__.py +0 -0
  108. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/base.py +0 -0
  109. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
  110. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/readers/json_reader.py +0 -0
  111. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/records.py +0 -0
  112. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/structure.py +0 -0
  113. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/technical.py +0 -0
  114. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/values.py +0 -0
  115. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/scanner/workers.py +0 -0
  116. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst/static/report.js +0 -0
  117. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  118. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/entry_points.txt +0 -0
  119. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/requires.txt +0 -0
  120. {tabalyst-0.4.3 → tabalyst-0.4.4}/src/tabalyst.egg-info/top_level.txt +0 -0
  121. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_cache.py +0 -0
  122. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_database.py +0 -0
  123. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_generations.py +0 -0
  124. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_identity.py +0 -0
  125. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_index.py +0 -0
  126. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_location.py +0 -0
  127. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_open_service.py +0 -0
  128. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_queries.py +0 -0
  129. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_rescan.py +0 -0
  130. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_scan.py +0 -0
  131. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_sessions.py +0 -0
  132. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_project_store.py +0 -0
  133. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_sampling.py +0 -0
  134. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_amounts.py +0 -0
  135. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_batches.py +0 -0
  136. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_config.py +0 -0
  137. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_csv.py +0 -0
  138. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_email_url.py +0 -0
  139. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_exposure.py +0 -0
  140. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_paths.py +0 -0
  141. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_phone.py +0 -0
  142. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_postal.py +0 -0
  143. {tabalyst-0.4.3 → tabalyst-0.4.4}/tests/test_scanner_uuid_ip.py +0 -0
  144. {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
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 output. It
161
- 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.
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 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
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 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.
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 project query caches for every project, or for one
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 project storage for CSV files. `from_scan=True`
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 output. It
125
- 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.
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 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
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 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.
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 project query caches for every project, or for one
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 project storage for CSV files. `from_scan=True`
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, 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.3"
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"
@@ -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 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
@@ -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 6).
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 len(listed) <= settings.full_distribution_max_distinct:
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 render_report(profile: ReportProfile) -> str:
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)