tabalyst 0.3.0__tar.gz → 0.4.1__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 (108) hide show
  1. {tabalyst-0.3.0/src/tabalyst.egg-info → tabalyst-0.4.1}/PKG-INFO +79 -14
  2. {tabalyst-0.3.0 → tabalyst-0.4.1}/README.md +76 -11
  3. {tabalyst-0.3.0 → tabalyst-0.4.1}/examples/README.md +12 -4
  4. tabalyst-0.4.1/examples/config.json +60 -0
  5. {tabalyst-0.3.0 → tabalyst-0.4.1}/pyproject.toml +6 -3
  6. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/__init__.py +10 -4
  7. tabalyst-0.4.1/src/tabalyst/batch.py +157 -0
  8. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/cli/app.py +2 -0
  9. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/cli/report.py +48 -63
  10. tabalyst-0.4.1/src/tabalyst/cli/scan.py +180 -0
  11. tabalyst-0.4.1/src/tabalyst/cli/terminal.py +56 -0
  12. tabalyst-0.4.1/src/tabalyst/config.py +262 -0
  13. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/execution_log.py +6 -4
  14. tabalyst-0.4.1/src/tabalyst/models.py +386 -0
  15. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/progress.py +7 -0
  16. tabalyst-0.4.1/src/tabalyst/report_config.py +72 -0
  17. tabalyst-0.4.1/src/tabalyst/report_profile.py +1084 -0
  18. tabalyst-0.4.1/src/tabalyst/report_service.py +268 -0
  19. tabalyst-0.4.1/src/tabalyst/reporting.py +130 -0
  20. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/sampling.py +2 -0
  21. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/sampling_service.py +15 -54
  22. tabalyst-0.4.1/src/tabalyst/scan_reuse.py +147 -0
  23. tabalyst-0.4.1/src/tabalyst/scan_service.py +184 -0
  24. tabalyst-0.4.1/src/tabalyst/scanner/__init__.py +12 -0
  25. tabalyst-0.4.1/src/tabalyst/scanner/api.py +175 -0
  26. tabalyst-0.4.1/src/tabalyst/scanner/config.py +525 -0
  27. tabalyst-0.4.1/src/tabalyst/scanner/detectors/__init__.py +16 -0
  28. tabalyst-0.4.1/src/tabalyst/scanner/detectors/amount.py +77 -0
  29. tabalyst-0.4.1/src/tabalyst/scanner/detectors/base.py +179 -0
  30. tabalyst-0.4.1/src/tabalyst/scanner/detectors/boolean.py +70 -0
  31. tabalyst-0.4.1/src/tabalyst/scanner/detectors/currency.py +135 -0
  32. tabalyst-0.4.1/src/tabalyst/scanner/detectors/email.py +87 -0
  33. tabalyst-0.4.1/src/tabalyst/scanner/detectors/enumeration.py +76 -0
  34. tabalyst-0.4.1/src/tabalyst/scanner/detectors/ip.py +129 -0
  35. tabalyst-0.4.1/src/tabalyst/scanner/detectors/names.py +87 -0
  36. tabalyst-0.4.1/src/tabalyst/scanner/detectors/number.py +161 -0
  37. tabalyst-0.4.1/src/tabalyst/scanner/detectors/pattern.py +48 -0
  38. tabalyst-0.4.1/src/tabalyst/scanner/detectors/percentage.py +43 -0
  39. tabalyst-0.4.1/src/tabalyst/scanner/detectors/phone.py +141 -0
  40. tabalyst-0.4.1/src/tabalyst/scanner/detectors/postal.py +100 -0
  41. tabalyst-0.4.1/src/tabalyst/scanner/detectors/quantity.py +101 -0
  42. tabalyst-0.4.1/src/tabalyst/scanner/detectors/registry.py +594 -0
  43. tabalyst-0.4.1/src/tabalyst/scanner/detectors/shape.py +36 -0
  44. tabalyst-0.4.1/src/tabalyst/scanner/detectors/temporal.py +400 -0
  45. tabalyst-0.4.1/src/tabalyst/scanner/detectors/url.py +145 -0
  46. tabalyst-0.4.1/src/tabalyst/scanner/detectors/uuid.py +98 -0
  47. tabalyst-0.4.1/src/tabalyst/scanner/diagnostics.py +82 -0
  48. tabalyst-0.4.1/src/tabalyst/scanner/engine.py +172 -0
  49. tabalyst-0.4.1/src/tabalyst/scanner/exposure.py +102 -0
  50. tabalyst-0.4.1/src/tabalyst/scanner/field.py +144 -0
  51. tabalyst-0.4.1/src/tabalyst/scanner/measures.py +457 -0
  52. tabalyst-0.4.1/src/tabalyst/scanner/models.py +536 -0
  53. tabalyst-0.4.1/src/tabalyst/scanner/normalization.py +99 -0
  54. tabalyst-0.4.1/src/tabalyst/scanner/observations.py +117 -0
  55. tabalyst-0.4.1/src/tabalyst/scanner/paths.py +142 -0
  56. tabalyst-0.4.1/src/tabalyst/scanner/readers/__init__.py +1 -0
  57. tabalyst-0.4.1/src/tabalyst/scanner/readers/base.py +78 -0
  58. tabalyst-0.4.1/src/tabalyst/scanner/readers/csv_reader.py +158 -0
  59. tabalyst-0.4.1/src/tabalyst/scanner/readers/json_reader.py +532 -0
  60. tabalyst-0.4.1/src/tabalyst/scanner/records.py +328 -0
  61. tabalyst-0.4.1/src/tabalyst/scanner/structure.py +344 -0
  62. tabalyst-0.4.1/src/tabalyst/scanner/technical.py +77 -0
  63. tabalyst-0.4.1/src/tabalyst/scanner/values.py +627 -0
  64. tabalyst-0.4.1/src/tabalyst/service.py +329 -0
  65. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/static/report.js +17 -3
  66. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/static/theme.css +24 -0
  67. tabalyst-0.4.1/src/tabalyst/templates/report.html +108 -0
  68. {tabalyst-0.3.0 → tabalyst-0.4.1/src/tabalyst.egg-info}/PKG-INFO +79 -14
  69. tabalyst-0.4.1/src/tabalyst.egg-info/SOURCES.txt +95 -0
  70. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst.egg-info/requires.txt +2 -2
  71. {tabalyst-0.3.0 → tabalyst-0.4.1}/tests/test_cli.py +17 -13
  72. {tabalyst-0.3.0 → tabalyst-0.4.1}/tests/test_public_api.py +2 -2
  73. {tabalyst-0.3.0 → tabalyst-0.4.1}/tests/test_reporting.py +4 -4
  74. {tabalyst-0.3.0 → tabalyst-0.4.1}/tests/test_sampling.py +16 -0
  75. tabalyst-0.4.1/tests/test_scan_command.py +416 -0
  76. tabalyst-0.4.1/tests/test_scanner_amounts.py +618 -0
  77. tabalyst-0.4.1/tests/test_scanner_config.py +85 -0
  78. tabalyst-0.4.1/tests/test_scanner_csv.py +170 -0
  79. tabalyst-0.4.1/tests/test_scanner_email_url.py +432 -0
  80. tabalyst-0.4.1/tests/test_scanner_exposure.py +263 -0
  81. tabalyst-0.4.1/tests/test_scanner_paths.py +72 -0
  82. tabalyst-0.4.1/tests/test_scanner_phone.py +308 -0
  83. tabalyst-0.4.1/tests/test_scanner_postal.py +270 -0
  84. tabalyst-0.4.1/tests/test_scanner_uuid_ip.py +518 -0
  85. tabalyst-0.3.0/examples/config.json +0 -48
  86. tabalyst-0.3.0/src/tabalyst/analysis.py +0 -883
  87. tabalyst-0.3.0/src/tabalyst/config.py +0 -214
  88. tabalyst-0.3.0/src/tabalyst/ingestion.py +0 -86
  89. tabalyst-0.3.0/src/tabalyst/models.py +0 -211
  90. tabalyst-0.3.0/src/tabalyst/report_service.py +0 -244
  91. tabalyst-0.3.0/src/tabalyst/reporting.py +0 -73
  92. tabalyst-0.3.0/src/tabalyst/service.py +0 -182
  93. tabalyst-0.3.0/src/tabalyst/templates/report.html +0 -83
  94. tabalyst-0.3.0/src/tabalyst.egg-info/SOURCES.txt +0 -41
  95. tabalyst-0.3.0/tests/test_analysis.py +0 -621
  96. {tabalyst-0.3.0 → tabalyst-0.4.1}/LICENSE +0 -0
  97. {tabalyst-0.3.0 → tabalyst-0.4.1}/MANIFEST.in +0 -0
  98. {tabalyst-0.3.0 → tabalyst-0.4.1}/examples/input/basic.csv +0 -0
  99. {tabalyst-0.3.0 → tabalyst-0.4.1}/examples/input/insurance-customers.csv +0 -0
  100. {tabalyst-0.3.0 → tabalyst-0.4.1}/setup.cfg +0 -0
  101. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/__main__.py +0 -0
  102. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/_version.py +0 -0
  103. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/cli/__init__.py +0 -0
  104. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/cli/sample.py +0 -0
  105. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst/errors.py +0 -0
  106. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  107. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst.egg-info/entry_points.txt +0 -0
  108. {tabalyst-0.3.0 → tabalyst-0.4.1}/src/tabalyst.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tabalyst
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Open-source toolkit for understanding and working with unfamiliar data.
5
5
  Author: Gregory Borelli
6
6
  License-Expression: MIT
@@ -21,9 +21,9 @@ 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: ijson>=3.3
24
25
  Requires-Dist: jinja2>=3.1
25
- Requires-Dist: pandas>=2.2
26
- Requires-Dist: pydantic>=2.10
26
+ Requires-Dist: pydantic>=2.11
27
27
  Requires-Dist: typer>=0.12
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: build>=1.2; extra == "dev"
@@ -65,6 +65,8 @@ changes often: update it before each new test.
65
65
  | One file, custom name | `tabalyst report data.csv -o report.html` | The file `report.html` |
66
66
  | Several files, automatic names | `tabalyst report *.csv` | Beside each source |
67
67
  | Several files, one directory | `tabalyst report *.csv -d reports/` | The directory `reports/` |
68
+ | One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
69
+ | From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
68
70
 
69
71
  The simplest command keeps the source filename:
70
72
 
@@ -87,6 +89,18 @@ Use `-o` to choose a different HTML filename for one source:
87
89
  tabalyst report customers.csv -o customer-analysis.html
88
90
  ```
89
91
 
92
+ ### JSON files
93
+
94
+ A JSON file gets a report too, named `<stem>.report.html` so its profile
95
+ `<stem>.report.json` never replaces the source. Each collection of records,
96
+ such as the `customers` array of `{"customers": [...]}`, is a dataset of the
97
+ report, and nested fields are columns named by their path, such as
98
+ `address.city`:
99
+
100
+ ```console
101
+ tabalyst report orders.json
102
+ ```
103
+
90
104
  ### Multiple files
91
105
 
92
106
  Report several CSV files at once:
@@ -201,16 +215,53 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
201
215
  only the requested sample, plus stratum counts, in memory. Existing outputs
202
216
  require `--force`, and an input file is never overwritten.
203
217
 
218
+ ## Tabalyst Scan
219
+
220
+ Describe every field of a CSV or JSON file in one JSON document:
221
+
222
+ ```console
223
+ tabalyst scan customers.csv
224
+ tabalyst scan orders.json --collection "$.customers[]"
225
+ ```
226
+
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
229
+ a stream, with memory bounded by configurable limits, and records for each
230
+ field its presence, native types, missing values, frequencies, exact
231
+ statistics, normalization variants, technical type and the result of every
232
+ detector: numbers with decimal commas, dates, booleans, enumerations, email
233
+ addresses, URLs, phone numbers, postal codes, currency amounts, percentages,
234
+ quantities, UUIDs and IP addresses. Values of sensitive fields, such as email
235
+ addresses, are masked by default. Results are written atomically: an
236
+ interrupted scan never leaves a partial file.
237
+
238
+ Build the report from a scan document without reading the source again:
239
+
240
+ ```console
241
+ tabalyst report --scan customers.scan.json
242
+ ```
243
+
244
+ Tabalyst refuses a scan whose source changed since it was written, or whose
245
+ settings differ from the `scan` settings of `--config`.
246
+
247
+ See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
248
+ and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
249
+
204
250
  ## What the report analyzes
205
251
 
206
252
  - Dataset dimensions, missing cells, duplicates, and quality observations.
207
253
  - Physical and semantic types with confidence and error rates.
208
254
  - Numeric, date, string-length, normalization, and value distributions.
209
- - Distinct values, representative examples, enum candidates, and date formats.
255
+ - Distinct values, representative examples, date formats, and semantic types
256
+ such as enumerations, email addresses, phone numbers, and postal codes.
210
257
  - CSV record widths, quoting, encoding, and delimiter configuration.
211
258
  - A bounded raw-data preview while every record is analyzed.
212
259
 
213
- The HTML report is self-contained and works without a CDN or network connection.
260
+ The report is built on Tabalyst Scan: it reads the CSV once as a stream and
261
+ masks values of sensitive columns, such as email addresses, by default.
262
+ Ambiguous dates stay ambiguous; the report shows the evidence of the column
263
+ without applying it. The HTML report is self-contained and works without a CDN
264
+ or network connection.
214
265
  The JSON profile contains the same canonical analysis result for scripts and
215
266
  future Tabalyst tools.
216
267
 
@@ -238,10 +289,17 @@ sample = tabalyst.sample_csv(
238
289
  rows=1000,
239
290
  seed=42,
240
291
  )
292
+
293
+ scan = tabalyst.scan("orders.json")
294
+ scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
295
+ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
241
296
  ```
242
297
 
243
298
  `analyze()` returns the JSON-serializable profile for one report.
244
299
  `generate_reports()` returns the complete batch plan, successes, and failures.
300
+ `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.
245
303
  Expected failures derive from `tabalyst.TabalystError`.
246
304
 
247
305
  Existing artifacts are never replaced silently. Pass `force=True` when
@@ -253,15 +311,21 @@ Configuration files are strict JSON. A minimal file is:
253
311
 
254
312
  ```json
255
313
  {
256
- "csv": {
257
- "delimiter": ";",
258
- "encoding": "cp1252"
314
+ "scan": {
315
+ "csv": {
316
+ "delimiter": ";",
317
+ "encoding": "cp1252"
318
+ },
319
+ "values": {"null_markers": ["N/A"]}
259
320
  }
260
321
  }
261
322
  ```
262
323
 
263
- Explicit CLI or Python arguments override the configuration file, which overrides
264
- Tabalyst defaults. See the
324
+ Analysis settings, CSV reading included, go in the `scan` object, shared by
325
+ `tabalyst report` and `tabalyst scan`. Top-level settings shape the report
326
+ presentation, and `csv` configures `tabalyst sample`. Explicit CLI or Python
327
+ arguments override the configuration file, which overrides Tabalyst defaults.
328
+ No configuration file is loaded unless it is passed with `--config`. See the
265
329
  [configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
266
330
  for all analysis settings.
267
331
 
@@ -271,10 +335,11 @@ Tabalyst performs analysis locally and adds no telemetry or remote processing.
271
335
  Generated JSON and HTML may contain source values and should be shared
272
336
  accordingly.
273
337
 
274
- Report analysis currently loads the complete CSV into memory. Sampling uses a
275
- streaming reader with bounded row storage. File and phase progress is available
276
- for reports; row percentages, throughput estimates, recursive directory input,
277
- and parallel batch execution will require later ingestion work.
338
+ Reports, samples, and scans read files as a stream, with memory bounded by
339
+ configurable limits, including duplicate-row detection
340
+ (`scan.limits.max_tracked_records`). Reports and scans show the share of the file read;
341
+ throughput estimates, recursive directory input, and parallel batch execution
342
+ will require later work.
278
343
 
279
344
  ## Examples and development
280
345
 
@@ -31,6 +31,8 @@ changes often: update it before each new test.
31
31
  | One file, custom name | `tabalyst report data.csv -o report.html` | The file `report.html` |
32
32
  | Several files, automatic names | `tabalyst report *.csv` | Beside each source |
33
33
  | Several files, one directory | `tabalyst report *.csv -d reports/` | The directory `reports/` |
34
+ | One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
35
+ | From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
34
36
 
35
37
  The simplest command keeps the source filename:
36
38
 
@@ -53,6 +55,18 @@ Use `-o` to choose a different HTML filename for one source:
53
55
  tabalyst report customers.csv -o customer-analysis.html
54
56
  ```
55
57
 
58
+ ### JSON files
59
+
60
+ A JSON file gets a report too, named `<stem>.report.html` so its profile
61
+ `<stem>.report.json` never replaces the source. Each collection of records,
62
+ such as the `customers` array of `{"customers": [...]}`, is a dataset of the
63
+ report, and nested fields are columns named by their path, such as
64
+ `address.city`:
65
+
66
+ ```console
67
+ tabalyst report orders.json
68
+ ```
69
+
56
70
  ### Multiple files
57
71
 
58
72
  Report several CSV files at once:
@@ -167,16 +181,53 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
167
181
  only the requested sample, plus stratum counts, in memory. Existing outputs
168
182
  require `--force`, and an input file is never overwritten.
169
183
 
184
+ ## Tabalyst Scan
185
+
186
+ Describe every field of a CSV or JSON file in one JSON document:
187
+
188
+ ```console
189
+ tabalyst scan customers.csv
190
+ tabalyst scan orders.json --collection "$.customers[]"
191
+ ```
192
+
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
195
+ a stream, with memory bounded by configurable limits, and records for each
196
+ field its presence, native types, missing values, frequencies, exact
197
+ statistics, normalization variants, technical type and the result of every
198
+ detector: numbers with decimal commas, dates, booleans, enumerations, email
199
+ addresses, URLs, phone numbers, postal codes, currency amounts, percentages,
200
+ quantities, UUIDs and IP addresses. Values of sensitive fields, such as email
201
+ addresses, are masked by default. Results are written atomically: an
202
+ interrupted scan never leaves a partial file.
203
+
204
+ Build the report from a scan document without reading the source again:
205
+
206
+ ```console
207
+ tabalyst report --scan customers.scan.json
208
+ ```
209
+
210
+ Tabalyst refuses a scan whose source changed since it was written, or whose
211
+ settings differ from the `scan` settings of `--config`.
212
+
213
+ See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
214
+ and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
215
+
170
216
  ## What the report analyzes
171
217
 
172
218
  - Dataset dimensions, missing cells, duplicates, and quality observations.
173
219
  - Physical and semantic types with confidence and error rates.
174
220
  - Numeric, date, string-length, normalization, and value distributions.
175
- - Distinct values, representative examples, enum candidates, and date formats.
221
+ - Distinct values, representative examples, date formats, and semantic types
222
+ such as enumerations, email addresses, phone numbers, and postal codes.
176
223
  - CSV record widths, quoting, encoding, and delimiter configuration.
177
224
  - A bounded raw-data preview while every record is analyzed.
178
225
 
179
- The HTML report is self-contained and works without a CDN or network connection.
226
+ The report is built on Tabalyst Scan: it reads the CSV once as a stream and
227
+ masks values of sensitive columns, such as email addresses, by default.
228
+ Ambiguous dates stay ambiguous; the report shows the evidence of the column
229
+ without applying it. The HTML report is self-contained and works without a CDN
230
+ or network connection.
180
231
  The JSON profile contains the same canonical analysis result for scripts and
181
232
  future Tabalyst tools.
182
233
 
@@ -204,10 +255,17 @@ sample = tabalyst.sample_csv(
204
255
  rows=1000,
205
256
  seed=42,
206
257
  )
258
+
259
+ scan = tabalyst.scan("orders.json")
260
+ scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
261
+ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
207
262
  ```
208
263
 
209
264
  `analyze()` returns the JSON-serializable profile for one report.
210
265
  `generate_reports()` returns the complete batch plan, successes, and failures.
266
+ `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.
211
269
  Expected failures derive from `tabalyst.TabalystError`.
212
270
 
213
271
  Existing artifacts are never replaced silently. Pass `force=True` when
@@ -219,15 +277,21 @@ Configuration files are strict JSON. A minimal file is:
219
277
 
220
278
  ```json
221
279
  {
222
- "csv": {
223
- "delimiter": ";",
224
- "encoding": "cp1252"
280
+ "scan": {
281
+ "csv": {
282
+ "delimiter": ";",
283
+ "encoding": "cp1252"
284
+ },
285
+ "values": {"null_markers": ["N/A"]}
225
286
  }
226
287
  }
227
288
  ```
228
289
 
229
- Explicit CLI or Python arguments override the configuration file, which overrides
230
- Tabalyst defaults. See the
290
+ Analysis settings, CSV reading included, go in the `scan` object, shared by
291
+ `tabalyst report` and `tabalyst scan`. Top-level settings shape the report
292
+ presentation, and `csv` configures `tabalyst sample`. Explicit CLI or Python
293
+ arguments override the configuration file, which overrides Tabalyst defaults.
294
+ No configuration file is loaded unless it is passed with `--config`. See the
231
295
  [configuration reference](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
232
296
  for all analysis settings.
233
297
 
@@ -237,10 +301,11 @@ Tabalyst performs analysis locally and adds no telemetry or remote processing.
237
301
  Generated JSON and HTML may contain source values and should be shared
238
302
  accordingly.
239
303
 
240
- Report analysis currently loads the complete CSV into memory. Sampling uses a
241
- streaming reader with bounded row storage. File and phase progress is available
242
- for reports; row percentages, throughput estimates, recursive directory input,
243
- and parallel batch execution will require later ingestion work.
304
+ Reports, samples, and scans read files as a stream, with memory bounded by
305
+ configurable limits, including duplicate-row detection
306
+ (`scan.limits.max_tracked_records`). Reports and scans show the share of the file read;
307
+ throughput estimates, recursive directory input, and parallel batch execution
308
+ will require later work.
244
309
 
245
310
  ## Examples and development
246
311
 
@@ -8,13 +8,18 @@ examples/
8
8
  |-- config.json
9
9
  |-- input/
10
10
  | |-- basic.csv
11
- | `-- insurance-customers.csv
11
+ | |-- insurance-customers.csv
12
+ | `-- orders.json
12
13
  `-- output/
13
14
  |-- basic/
14
15
  | |-- report.html
15
16
  | |-- report.json
16
17
  | `-- executions.json
17
- `-- insurance-customers/
18
+ |-- insurance-customers/
19
+ | |-- report.html
20
+ | |-- report.json
21
+ | `-- executions.json
22
+ `-- orders/
18
23
  |-- report.html
19
24
  |-- report.json
20
25
  `-- executions.json
@@ -23,13 +28,16 @@ examples/
23
28
  `basic.csv` is a five-row smoke example. `insurance-customers.csv` contains 3,000
24
29
  synthetic customer and contract records across 34 columns. Its people, contact
25
30
  details, and contracts are fictional; email addresses use reserved `.example`
26
- domains.
31
+ domains. `orders.json` is a synthetic shop export: 60 customers with nested
32
+ addresses, tags and orders, whose report has two datasets, `$` and
33
+ `$.customers[]`.
27
34
 
28
- From the repository root, regenerate both outputs with:
35
+ From the repository root, regenerate the outputs with:
29
36
 
30
37
  ```console
31
38
  tabalyst report examples/input/basic.csv -o examples/output/basic/report.html --config examples/config.json --force
32
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/orders.json -o examples/output/orders/report.html --config examples/config.json --force
33
41
  ```
34
42
 
35
43
  Every output folder is independent and contains its own HTML report, canonical
@@ -0,0 +1,60 @@
1
+ {
2
+ "string_analysis": {
3
+ "very_short_max_length": 5,
4
+ "short_max_length": 20,
5
+ "medium_max_length": 50,
6
+ "long_max_length": 255,
7
+ "length_distribution_max_length": 50,
8
+ "examples_per_length": 10
9
+ },
10
+ "value_examples": {
11
+ "full_distribution_max_distinct": 50,
12
+ "short_text_max_length": 20,
13
+ "short_text_percentile": 0.95,
14
+ "short_text_result_size": 20,
15
+ "long_text_result_size": 20,
16
+ "long_text_truncate_at": 30,
17
+ "truncation_suffix": "...",
18
+ "inline_display_size": 3
19
+ },
20
+ "scan": {
21
+ "csv": {
22
+ "encoding": "utf-8-sig",
23
+ "delimiter": ","
24
+ },
25
+ "values": {
26
+ "null_markers": ["N/A", "NULL"],
27
+ "missing": ["absent", "null", "empty", "blank", "marker"]
28
+ },
29
+ "normalization": {
30
+ "trim": true,
31
+ "collapse_whitespace": true
32
+ },
33
+ "types": {
34
+ "minimum_confidence": 0.95
35
+ },
36
+ "detectors": {
37
+ "date": {
38
+ "orders": ["YMD", "MDY", "DMY"],
39
+ "separators": ["-", "/", "."],
40
+ "ambiguous_order": null
41
+ },
42
+ "enumeration": {
43
+ "minimum_values": 500,
44
+ "maximum_distinct": 49,
45
+ "case_sensitive": true
46
+ }
47
+ },
48
+ "limits": {
49
+ "max_samples": 100
50
+ },
51
+ "records": {
52
+ "preview": 20,
53
+ "duplicates": true
54
+ },
55
+ "exposure": {
56
+ "sensitive_values": "mask"
57
+ },
58
+ "random_seed": 42
59
+ }
60
+ }
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabalyst"
7
- version = "0.3.0"
7
+ version = "0.4.1"
8
8
  description = "Open-source toolkit for understanding and working with unfamiliar data."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -34,9 +34,9 @@ classifiers = [
34
34
  "Topic :: Scientific/Engineering :: Information Analysis",
35
35
  ]
36
36
  dependencies = [
37
+ "ijson>=3.3",
37
38
  "jinja2>=3.1",
38
- "pandas>=2.2",
39
- "pydantic>=2.10",
39
+ "pydantic>=2.11",
40
40
  "typer>=0.12",
41
41
  ]
42
42
 
@@ -65,6 +65,9 @@ Issues = "https://github.com/loribel-labs/tabalyst/issues"
65
65
  [tool.pytest.ini_options]
66
66
  testpaths = ["tests"]
67
67
  pythonpath = ["src"]
68
+ markers = [
69
+ "lot(name): Tabalyst Scan implementation lot that provides the tested behavior",
70
+ ]
68
71
 
69
72
  [tool.ruff]
70
73
  line-length = 88
@@ -1,28 +1,34 @@
1
1
  """Public Python interface for Tabalyst."""
2
2
 
3
3
  from tabalyst._version import __version__
4
- from tabalyst.analysis import analyze_column as analyze_column
5
- from tabalyst.config import AnalysisConfig as AnalysisConfig
6
4
  from tabalyst.errors import ConfigurationError, InputError, ReportError, TabalystError
5
+ from tabalyst.report_config import ReportConfig as ReportConfig
7
6
  from tabalyst.report_service import generate_reports as generate_reports
8
7
  from tabalyst.reporting import render_report as render_report
9
8
  from tabalyst.sampling import SampleMethod as SampleMethod
10
9
  from tabalyst.sampling import SampleResult as SampleResult
11
10
  from tabalyst.sampling import sample_csv as sample_csv
12
11
  from tabalyst.sampling_service import generate_samples as generate_samples
12
+ from tabalyst.scan_service import generate_scans as generate_scans
13
+ from tabalyst.scanner import ScanConfig as ScanConfig
14
+ from tabalyst.scanner import ScanResult as ScanResult
15
+ from tabalyst.scanner import scan as scan
13
16
  from tabalyst.service import analyze
14
17
  from tabalyst.service import analyze_csv as analyze_csv
18
+ from tabalyst.service import analyze_scan as analyze_scan
15
19
 
16
20
  __all__ = [
17
21
  "ConfigurationError",
18
22
  "InputError",
19
23
  "ReportError",
24
+ "ScanConfig",
25
+ "ScanResult",
20
26
  "TabalystError",
21
27
  "__version__",
22
28
  "analyze",
23
29
  "generate_reports",
24
30
  "generate_samples",
31
+ "generate_scans",
25
32
  "sample_csv",
33
+ "scan",
26
34
  ]
27
-
28
- # Alpha compatibility imports remain available, but are not part of the 0.1.0 API.
@@ -0,0 +1,157 @@
1
+ """Batch planning shared by the commands: inputs, outputs and safe writing.
2
+
3
+ Every command resolves its inputs, maps each one to its outputs and rejects the
4
+ whole batch before any work when the plan is unsafe: output name collisions,
5
+ outputs that would replace an input, or existing outputs without ``force``.
6
+ """
7
+
8
+ import glob
9
+ import os
10
+ import tempfile
11
+ from collections.abc import Callable, Iterable, Sequence
12
+ from pathlib import Path
13
+
14
+ from tabalyst.errors import ConfigurationError, InputError, ReportError
15
+
16
+
17
+ def path_key(path: Path) -> str:
18
+ """Identity of a path for comparisons, whatever its spelling and case."""
19
+ return os.path.normcase(str(path.resolve()))
20
+
21
+
22
+ def resolve_input_specs(input_specs: Sequence[str | Path]) -> list[Path]:
23
+ """Resolve explicit files and shell-independent, non-recursive glob patterns."""
24
+ if not input_specs:
25
+ raise ConfigurationError("At least one input file is required.")
26
+
27
+ sources: list[Path] = []
28
+ known: set[str] = set()
29
+ for value in input_specs:
30
+ text = os.fspath(value)
31
+ if "**" in text:
32
+ raise ConfigurationError(
33
+ f"Recursive input patterns are not supported yet: {text}"
34
+ )
35
+ if glob.has_magic(text):
36
+ matches = sorted(
37
+ (Path(match) for match in glob.glob(text) if Path(match).is_file()),
38
+ key=path_key,
39
+ )
40
+ if not matches:
41
+ raise InputError(f'Input pattern matched no files: "{text}"')
42
+ else:
43
+ path = Path(text)
44
+ if not path.exists():
45
+ raise InputError(f"Input file does not exist: {path}")
46
+ if not path.is_file():
47
+ raise InputError(f"Input path is not a file: {path}")
48
+ matches = [path]
49
+
50
+ for path in matches:
51
+ key = path_key(path)
52
+ if key not in known:
53
+ sources.append(path)
54
+ known.add(key)
55
+ return sources
56
+
57
+
58
+ def plan_outputs(
59
+ input_specs: Sequence[str | Path],
60
+ *,
61
+ output: str | Path | None,
62
+ output_dir: str | Path | None,
63
+ output_name: Callable[[Path], str],
64
+ ) -> list[tuple[Path, Path]]:
65
+ """Resolve the inputs and pair each one with its main output.
66
+
67
+ ``output`` names the output of a single input; otherwise each output is
68
+ named ``output_name(source)``, in ``output_dir`` or beside its source.
69
+ """
70
+ if output is not None and output_dir is not None:
71
+ raise ConfigurationError("--output and --output-dir cannot be used together.")
72
+ sources = resolve_input_specs(input_specs)
73
+ if output is not None and len(sources) != 1:
74
+ raise ConfigurationError("--output can only be used with one input file.")
75
+
76
+ destination = Path(output_dir) if output_dir is not None else None
77
+ if destination is not None and destination.exists() and not destination.is_dir():
78
+ raise ReportError(f"Output directory path is a file: {destination}")
79
+ return [
80
+ (
81
+ source,
82
+ Path(output)
83
+ if output is not None
84
+ else (destination if destination is not None else source.parent)
85
+ / output_name(source),
86
+ )
87
+ for source in sources
88
+ ]
89
+
90
+
91
+ def validate_outputs(
92
+ artifacts: Iterable[tuple[Path, Path]],
93
+ *,
94
+ sources: Iterable[Path],
95
+ force: bool,
96
+ label: str,
97
+ ) -> None:
98
+ """Reject unsafe ``(source, output)`` pairs of a whole batch.
99
+
100
+ ``label`` names the outputs in messages, such as ``"Report"``.
101
+ """
102
+ input_keys = {path_key(source) for source in sources}
103
+ owners: dict[str, Path] = {}
104
+ existing: list[Path] = []
105
+ for source, artifact in artifacts:
106
+ if artifact.exists() and artifact.is_dir():
107
+ raise InputError(f"{label} path is a directory: {artifact}")
108
+ key = path_key(artifact)
109
+ if key in input_keys:
110
+ raise InputError(f"{label} would overwrite an input file: {artifact}")
111
+ previous = owners.get(key)
112
+ if previous is not None:
113
+ raise ReportError(
114
+ "Output name collision: "
115
+ f"{previous} and {source} both map to {artifact}."
116
+ )
117
+ owners[key] = source
118
+ if not force and artifact.exists():
119
+ existing.append(artifact)
120
+ if existing:
121
+ lines = "\n".join(f" {path}" for path in existing)
122
+ raise ReportError(
123
+ f"{label} output already exists. Use --force to replace it:\n" + lines
124
+ )
125
+
126
+
127
+ def apply_default_file_mode(path: Path) -> None:
128
+ """Give ``path`` the permissions of a plain write under the current umask.
129
+
130
+ Temporary files are created for their owner only, and ``os.replace`` keeps
131
+ that mode on the final file.
132
+ """
133
+ umask = os.umask(0)
134
+ os.umask(umask)
135
+ os.chmod(path, 0o666 & ~umask)
136
+
137
+
138
+ def write_text_atomic(path: Path, content: str) -> None:
139
+ """Write through a temporary file in the target directory, then replace.
140
+
141
+ An interrupted write never leaves a partial file at ``path``.
142
+ """
143
+ path.parent.mkdir(parents=True, exist_ok=True)
144
+ descriptor, name = tempfile.mkstemp(
145
+ dir=path.parent, prefix=f".{path.name}.", suffix=".tmp"
146
+ )
147
+ temporary = Path(name)
148
+ try:
149
+ apply_default_file_mode(temporary)
150
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
151
+ handle.write(content)
152
+ handle.flush()
153
+ os.fsync(handle.fileno())
154
+ os.replace(temporary, path)
155
+ except BaseException:
156
+ temporary.unlink(missing_ok=True)
157
+ raise
@@ -7,6 +7,7 @@ import typer
7
7
  from tabalyst import __version__
8
8
  from tabalyst.cli.report import report_command
9
9
  from tabalyst.cli.sample import sample_command
10
+ from tabalyst.cli.scan import scan_command
10
11
 
11
12
  app = typer.Typer(
12
13
  add_completion=False,
@@ -39,6 +40,7 @@ def root(
39
40
 
40
41
  app.command("report")(report_command)
41
42
  app.command("sample")(sample_command)
43
+ app.command("scan")(scan_command)
42
44
 
43
45
 
44
46
  def main() -> None: