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.
Files changed (143) hide show
  1. {tabalyst-0.4.1/src/tabalyst.egg-info → tabalyst-0.4.3}/PKG-INFO +39 -10
  2. {tabalyst-0.4.1 → tabalyst-0.4.3}/README.md +35 -8
  3. {tabalyst-0.4.1 → tabalyst-0.4.3}/pyproject.toml +4 -2
  4. tabalyst-0.4.3/src/tabalyst/cache_service.py +114 -0
  5. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/app.py +2 -0
  6. tabalyst-0.4.3/src/tabalyst/cli/cache.py +64 -0
  7. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/report.py +14 -0
  8. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/scan.py +15 -0
  9. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/models.py +1 -1
  10. tabalyst-0.4.3/src/tabalyst/project_open_service.py +179 -0
  11. tabalyst-0.4.3/src/tabalyst/project_scan_service.py +204 -0
  12. tabalyst-0.4.3/src/tabalyst/projects/__init__.py +48 -0
  13. tabalyst-0.4.3/src/tabalyst/projects/_bounded_values.py +449 -0
  14. tabalyst-0.4.3/src/tabalyst/projects/_cache.py +370 -0
  15. tabalyst-0.4.3/src/tabalyst/projects/_codec.py +116 -0
  16. tabalyst-0.4.3/src/tabalyst/projects/_generation.py +118 -0
  17. tabalyst-0.4.3/src/tabalyst/projects/_locks.py +144 -0
  18. tabalyst-0.4.3/src/tabalyst/projects/_publication.py +278 -0
  19. tabalyst-0.4.3/src/tabalyst/projects/_queries.py +481 -0
  20. tabalyst-0.4.3/src/tabalyst/projects/_query_budget.py +83 -0
  21. tabalyst-0.4.3/src/tabalyst/projects/_recovery.py +59 -0
  22. tabalyst-0.4.3/src/tabalyst/projects/_retention.py +70 -0
  23. tabalyst-0.4.3/src/tabalyst/projects/_schema.py +115 -0
  24. tabalyst-0.4.3/src/tabalyst/projects/_session.py +226 -0
  25. tabalyst-0.4.3/src/tabalyst/projects/_staging.py +439 -0
  26. tabalyst-0.4.3/src/tabalyst/projects/_sync.py +42 -0
  27. tabalyst-0.4.3/src/tabalyst/projects/_validation.py +459 -0
  28. tabalyst-0.4.3/src/tabalyst/projects/catalog.py +75 -0
  29. tabalyst-0.4.3/src/tabalyst/projects/freshness.py +54 -0
  30. tabalyst-0.4.3/src/tabalyst/projects/identity.py +42 -0
  31. tabalyst-0.4.3/src/tabalyst/projects/index.py +175 -0
  32. tabalyst-0.4.3/src/tabalyst/projects/location.py +118 -0
  33. tabalyst-0.4.3/src/tabalyst/projects/models.py +110 -0
  34. tabalyst-0.4.3/src/tabalyst/projects/store.py +117 -0
  35. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/report_service.py +21 -2
  36. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/reporting.py +14 -0
  37. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scan_reuse.py +51 -16
  38. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scan_service.py +68 -24
  39. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/api.py +35 -4
  40. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/config.py +3 -0
  41. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/base.py +23 -4
  42. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/boolean.py +9 -1
  43. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/currency.py +10 -1
  44. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/email.py +5 -1
  45. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/enumeration.py +12 -2
  46. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/ip.py +12 -1
  47. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/number.py +23 -1
  48. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/pattern.py +7 -1
  49. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/percentage.py +5 -1
  50. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/phone.py +12 -1
  51. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/postal.py +5 -1
  52. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/quantity.py +5 -1
  53. tabalyst-0.4.3/src/tabalyst/scanner/detectors/registry.py +832 -0
  54. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/temporal.py +18 -1
  55. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/url.py +8 -1
  56. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/uuid.py +5 -0
  57. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/engine.py +22 -1
  58. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/exposure.py +18 -1
  59. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/field.py +53 -13
  60. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/measures.py +139 -109
  61. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/models.py +7 -4
  62. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/normalization.py +43 -9
  63. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/observations.py +41 -6
  64. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/csv_reader.py +24 -14
  65. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/json_reader.py +32 -4
  66. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/records.py +87 -22
  67. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/structure.py +77 -6
  68. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/technical.py +15 -22
  69. tabalyst-0.4.3/src/tabalyst/scanner/values.py +1022 -0
  70. tabalyst-0.4.3/src/tabalyst/scanner/workers.py +391 -0
  71. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/service.py +39 -6
  72. tabalyst-0.4.3/src/tabalyst/shared_scan_service.py +149 -0
  73. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/static/report.js +35 -24
  74. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/static/theme.css +10 -4
  75. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/templates/report.html +14 -8
  76. {tabalyst-0.4.1 → tabalyst-0.4.3/src/tabalyst.egg-info}/PKG-INFO +39 -10
  77. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/SOURCES.txt +45 -1
  78. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/requires.txt +2 -0
  79. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_cli.py +7 -2
  80. tabalyst-0.4.3/tests/test_project_cache.py +439 -0
  81. tabalyst-0.4.3/tests/test_project_database.py +525 -0
  82. tabalyst-0.4.3/tests/test_project_generations.py +481 -0
  83. tabalyst-0.4.3/tests/test_project_identity.py +55 -0
  84. tabalyst-0.4.3/tests/test_project_index.py +172 -0
  85. tabalyst-0.4.3/tests/test_project_location.py +89 -0
  86. tabalyst-0.4.3/tests/test_project_open_service.py +300 -0
  87. tabalyst-0.4.3/tests/test_project_queries.py +718 -0
  88. tabalyst-0.4.3/tests/test_project_rescan.py +536 -0
  89. tabalyst-0.4.3/tests/test_project_scan.py +238 -0
  90. tabalyst-0.4.3/tests/test_project_sessions.py +441 -0
  91. tabalyst-0.4.3/tests/test_project_store.py +238 -0
  92. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_public_api.py +12 -15
  93. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_reporting.py +19 -1
  94. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scan_command.py +97 -16
  95. tabalyst-0.4.3/tests/test_scanner_batches.py +248 -0
  96. tabalyst-0.4.3/tests/test_shared_scan_cli.py +130 -0
  97. tabalyst-0.4.3/tests/test_source_freshness.py +79 -0
  98. tabalyst-0.4.1/src/tabalyst/scanner/detectors/registry.py +0 -594
  99. tabalyst-0.4.1/src/tabalyst/scanner/values.py +0 -627
  100. {tabalyst-0.4.1 → tabalyst-0.4.3}/LICENSE +0 -0
  101. {tabalyst-0.4.1 → tabalyst-0.4.3}/MANIFEST.in +0 -0
  102. {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/README.md +0 -0
  103. {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/config.json +0 -0
  104. {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/input/basic.csv +0 -0
  105. {tabalyst-0.4.1 → tabalyst-0.4.3}/examples/input/insurance-customers.csv +0 -0
  106. {tabalyst-0.4.1 → tabalyst-0.4.3}/setup.cfg +0 -0
  107. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/__init__.py +0 -0
  108. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/__main__.py +0 -0
  109. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/_version.py +0 -0
  110. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/batch.py +0 -0
  111. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/__init__.py +0 -0
  112. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/sample.py +0 -0
  113. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/cli/terminal.py +0 -0
  114. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/config.py +0 -0
  115. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/errors.py +0 -0
  116. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/execution_log.py +0 -0
  117. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/progress.py +0 -0
  118. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/report_config.py +0 -0
  119. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/report_profile.py +0 -0
  120. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/sampling.py +0 -0
  121. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/sampling_service.py +0 -0
  122. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/__init__.py +0 -0
  123. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/__init__.py +0 -0
  124. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/amount.py +0 -0
  125. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/names.py +0 -0
  126. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/shape.py +0 -0
  127. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/diagnostics.py +0 -0
  128. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/paths.py +0 -0
  129. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/__init__.py +0 -0
  130. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/base.py +0 -0
  131. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  132. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/entry_points.txt +0 -0
  133. {tabalyst-0.4.1 → tabalyst-0.4.3}/src/tabalyst.egg-info/top_level.txt +0 -0
  134. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_sampling.py +0 -0
  135. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_amounts.py +0 -0
  136. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_config.py +0 -0
  137. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_csv.py +0 -0
  138. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_email_url.py +0 -0
  139. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_exposure.py +0 -0
  140. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_paths.py +0 -0
  141. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_phone.py +0 -0
  142. {tabalyst-0.4.1 → tabalyst-0.4.3}/tests/test_scanner_postal.py +0 -0
  143. {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.1
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 :: 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
@@ -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
- The default output is `customers.scan.json` beside the source; `-o`, `-d` and
228
- `--force` work as for the other commands. Tabalyst Scan reads the file once as
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
- Build the report from a scan document without reading the source again:
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; `from_scan=True` builds reports
302
- from such documents.
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 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
@@ -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
- The default output is `customers.scan.json` beside the source; `-o`, `-d` and
194
- `--force` work as for the other commands. Tabalyst Scan reads the file once as
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
- Build the report from a scan document without reading the source again:
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; `from_scan=True` builds reports
268
- from such documents.
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.1"
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 :: 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 "
@@ -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[7] = 7
381
+ format_revision: Literal[8] = 8
382
382
  generated_at: datetime
383
383
  processing_seconds: FiniteFloat
384
384
  source: SourceInfo