tabalyst 0.4.3__tar.gz → 0.5.0__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 (157) hide show
  1. {tabalyst-0.4.3/src/tabalyst.egg-info → tabalyst-0.5.0}/PKG-INFO +94 -27
  2. {tabalyst-0.4.3 → tabalyst-0.5.0}/README.md +93 -26
  3. {tabalyst-0.4.3 → tabalyst-0.5.0}/examples/README.md +20 -6
  4. {tabalyst-0.4.3 → tabalyst-0.5.0}/pyproject.toml +2 -1
  5. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/__init__.py +6 -0
  6. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/batch.py +24 -2
  7. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/app.py +9 -2
  8. tabalyst-0.5.0/src/tabalyst/cli/inspect.py +161 -0
  9. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/report.py +8 -0
  10. tabalyst-0.5.0/src/tabalyst/inspect_service.py +209 -0
  11. tabalyst-0.5.0/src/tabalyst/inspector/__init__.py +5 -0
  12. tabalyst-0.5.0/src/tabalyst/inspector/json_inspect/__init__.py +10 -0
  13. tabalyst-0.5.0/src/tabalyst/inspector/json_inspect/build.py +314 -0
  14. tabalyst-0.5.0/src/tabalyst/inspector/json_inspect/detect.py +352 -0
  15. tabalyst-0.5.0/src/tabalyst/inspector/json_inspect/parameters.py +16 -0
  16. tabalyst-0.5.0/src/tabalyst/inspector/models.py +283 -0
  17. tabalyst-0.5.0/src/tabalyst/inspector/persistence.py +349 -0
  18. tabalyst-0.5.0/src/tabalyst/inspector/resolution.py +386 -0
  19. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/models.py +44 -6
  20. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/progress.py +30 -0
  21. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/project_open_service.py +6 -2
  22. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_session.py +27 -14
  23. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_staging.py +11 -4
  24. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/freshness.py +16 -3
  25. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/location.py +15 -0
  26. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/report_profile.py +55 -5
  27. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/report_service.py +9 -3
  28. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/reporting.py +53 -2
  29. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scan_reuse.py +41 -12
  30. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scan_service.py +36 -22
  31. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/api.py +15 -30
  32. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/config.py +99 -4
  33. tabalyst-0.5.0/src/tabalyst/scanner/identity.py +56 -0
  34. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/models.py +2 -2
  35. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/paths.py +24 -13
  36. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/readers/base.py +1 -1
  37. tabalyst-0.5.0/src/tabalyst/scanner/readers/json_common.py +37 -0
  38. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/readers/json_reader.py +65 -65
  39. tabalyst-0.5.0/src/tabalyst/scanner/readers/jsonl_reader.py +431 -0
  40. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/structure.py +1 -1
  41. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/service.py +128 -22
  42. tabalyst-0.5.0/src/tabalyst/shared_scan_service.py +91 -0
  43. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/static/theme.css +17 -0
  44. tabalyst-0.5.0/src/tabalyst/templates/column.html +66 -0
  45. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/templates/report.html +4 -4
  46. {tabalyst-0.4.3 → tabalyst-0.5.0/src/tabalyst.egg-info}/PKG-INFO +94 -27
  47. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst.egg-info/SOURCES.txt +14 -0
  48. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_cli.py +2 -2
  49. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_database.py +9 -0
  50. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_open_service.py +4 -1
  51. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_queries.py +2 -1
  52. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_rescan.py +8 -3
  53. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_sessions.py +5 -3
  54. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_public_api.py +22 -0
  55. tabalyst-0.5.0/tests/test_reporting.py +296 -0
  56. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scan_command.py +6 -13
  57. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_config.py +6 -1
  58. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_shared_scan_cli.py +33 -24
  59. tabalyst-0.4.3/src/tabalyst/shared_scan_service.py +0 -149
  60. tabalyst-0.4.3/tests/test_reporting.py +0 -137
  61. {tabalyst-0.4.3 → tabalyst-0.5.0}/LICENSE +0 -0
  62. {tabalyst-0.4.3 → tabalyst-0.5.0}/MANIFEST.in +0 -0
  63. {tabalyst-0.4.3 → tabalyst-0.5.0}/examples/config.json +0 -0
  64. {tabalyst-0.4.3 → tabalyst-0.5.0}/examples/input/basic.csv +0 -0
  65. {tabalyst-0.4.3 → tabalyst-0.5.0}/examples/input/insurance-customers.csv +0 -0
  66. {tabalyst-0.4.3 → tabalyst-0.5.0}/setup.cfg +0 -0
  67. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/__main__.py +0 -0
  68. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/_version.py +0 -0
  69. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cache_service.py +0 -0
  70. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/__init__.py +0 -0
  71. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/cache.py +0 -0
  72. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/sample.py +0 -0
  73. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/scan.py +0 -0
  74. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/cli/terminal.py +0 -0
  75. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/config.py +0 -0
  76. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/errors.py +0 -0
  77. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/execution_log.py +0 -0
  78. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/project_scan_service.py +0 -0
  79. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/__init__.py +0 -0
  80. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_bounded_values.py +0 -0
  81. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_cache.py +0 -0
  82. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_codec.py +0 -0
  83. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_generation.py +0 -0
  84. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_locks.py +0 -0
  85. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_publication.py +0 -0
  86. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_queries.py +0 -0
  87. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_query_budget.py +0 -0
  88. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_recovery.py +0 -0
  89. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_retention.py +0 -0
  90. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_schema.py +0 -0
  91. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_sync.py +0 -0
  92. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/_validation.py +0 -0
  93. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/catalog.py +0 -0
  94. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/identity.py +0 -0
  95. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/index.py +0 -0
  96. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/models.py +0 -0
  97. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/projects/store.py +0 -0
  98. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/report_config.py +0 -0
  99. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/sampling.py +0 -0
  100. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/sampling_service.py +0 -0
  101. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/__init__.py +0 -0
  102. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/__init__.py +0 -0
  103. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/amount.py +0 -0
  104. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/base.py +0 -0
  105. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/boolean.py +0 -0
  106. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/currency.py +0 -0
  107. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/email.py +0 -0
  108. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
  109. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/ip.py +0 -0
  110. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/names.py +0 -0
  111. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/number.py +0 -0
  112. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/pattern.py +0 -0
  113. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/percentage.py +0 -0
  114. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/phone.py +0 -0
  115. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/postal.py +0 -0
  116. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/quantity.py +0 -0
  117. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/registry.py +0 -0
  118. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/shape.py +0 -0
  119. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/temporal.py +0 -0
  120. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/url.py +0 -0
  121. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/detectors/uuid.py +0 -0
  122. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/diagnostics.py +0 -0
  123. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/engine.py +0 -0
  124. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/exposure.py +0 -0
  125. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/field.py +0 -0
  126. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/measures.py +0 -0
  127. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/normalization.py +0 -0
  128. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/observations.py +0 -0
  129. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/readers/__init__.py +0 -0
  130. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
  131. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/records.py +0 -0
  132. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/technical.py +0 -0
  133. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/values.py +0 -0
  134. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/scanner/workers.py +0 -0
  135. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst/static/report.js +0 -0
  136. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  137. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst.egg-info/entry_points.txt +0 -0
  138. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst.egg-info/requires.txt +0 -0
  139. {tabalyst-0.4.3 → tabalyst-0.5.0}/src/tabalyst.egg-info/top_level.txt +0 -0
  140. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_cache.py +0 -0
  141. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_generations.py +0 -0
  142. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_identity.py +0 -0
  143. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_index.py +0 -0
  144. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_location.py +0 -0
  145. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_scan.py +0 -0
  146. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_project_store.py +0 -0
  147. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_sampling.py +0 -0
  148. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_amounts.py +0 -0
  149. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_batches.py +0 -0
  150. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_csv.py +0 -0
  151. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_email_url.py +0 -0
  152. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_exposure.py +0 -0
  153. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_paths.py +0 -0
  154. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_phone.py +0 -0
  155. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_postal.py +0 -0
  156. {tabalyst-0.4.3 → tabalyst-0.5.0}/tests/test_scanner_uuid_ip.py +0 -0
  157. {tabalyst-0.4.3 → tabalyst-0.5.0}/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.5.0
4
4
  Summary: Open-source toolkit for understanding and working with unfamiliar data.
5
5
  Author: Gregory Borelli
6
6
  License-Expression: MIT
@@ -44,7 +44,8 @@ with structured data.
44
44
  Its first tool is **Tabalyst Report**. The current CSV implementation,
45
45
  **Tabalyst CSV Report**, analyzes a CSV file and produces both a structured JSON
46
46
  profile and a self-contained interactive HTML report.
47
- **Tabalyst Scan** describes CSV and JSON files in a scan document, and
47
+ **Tabalyst Scan** describes CSV, JSON and JSONL files in a scan document,
48
+ **Tabalyst Inspect** finds how to read a JSON or JSONL file, and
48
49
  **Tabalyst Sample** creates smaller CSV files.
49
50
 
50
51
  Tabalyst is in beta. Its interfaces may still change while
@@ -70,6 +71,7 @@ changes often: update it before each new test.
70
71
  | Several files, automatic names | `tabalyst report *.csv` | Beside each source |
71
72
  | Several files, one directory | `tabalyst report *.csv -d reports/` | The directory `reports/` |
72
73
  | One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
74
+ | One JSONL file | `tabalyst report events.jsonl` | `events.report.html` beside the source |
73
75
  | From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
74
76
 
75
77
  The simplest command keeps the source filename:
@@ -87,6 +89,20 @@ customers.json
87
89
  executions.json
88
90
  ```
89
91
 
92
+ Add `--details` to generate one self-contained HTML page per column:
93
+
94
+ ```console
95
+ tabalyst report customers.csv --details
96
+ ```
97
+
98
+ The **Columns** table then links to those pages. Each page has its own sidebar
99
+ and shows values, counts, detected formats and other analysis directly. The
100
+ subfolder takes the HTML filename without `.html`; for `-o report.html`, pages
101
+ are in `report/`. Names use `col-01-` and a shortened slug of the column name,
102
+ in source order. Details are off by default. `--no-details` states that choice
103
+ explicitly; with `--force`, it removes prior generated column pages while
104
+ preserving other files in the folder.
105
+
90
106
  Use `-o` to choose a different HTML filename for one source:
91
107
 
92
108
  ```console
@@ -95,16 +111,23 @@ tabalyst report customers.csv -o customer-analysis.html
95
111
 
96
112
  ### JSON files
97
113
 
98
- A JSON file gets a report too, named `<stem>.report.html` so its profile
99
- `<stem>.report.json` never replaces the source. Each collection of records,
100
- such as the `customers` array of `{"customers": [...]}`, is a dataset of the
101
- report, and nested fields are columns named by their path, such as
102
- `address.city`:
114
+ A JSON or JSONL file gets a report too, named `<stem>.report.html` so its
115
+ profile `<stem>.report.json` never replaces the source. The report analyzes one
116
+ collection of records, such as the `customers` array of
117
+ `{"customers": [...]}`, chosen by Tabalyst Inspect, and nested fields are
118
+ columns named by their path, such as `address.city`:
103
119
 
104
120
  ```console
105
121
  tabalyst report orders.json
122
+ tabalyst report events.jsonl
106
123
  ```
107
124
 
125
+ In a JSONL file (`.jsonl` or `.ndjson`), each line is a record. A line that is
126
+ not valid JSON or not an object is excluded and counted, and the report is
127
+ partial. When a JSON file holds several arrays that are equally plausible,
128
+ Tabalyst stops and lists them instead of choosing: see
129
+ [Tabalyst Inspect](#tabalyst-inspect).
130
+
108
131
  ### Multiple files
109
132
 
110
133
  Report several CSV files at once:
@@ -157,8 +180,10 @@ instead.
157
180
 
158
181
  ### Safe batch behavior
159
182
 
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.
183
+ Before processing begins, Tabalyst resolves every input and planned main HTML
184
+ and JSON output. It stops the entire batch if those names collide or if an
185
+ output already exists. With `--details`, each column page is checked after its
186
+ profile has been built, before that report's files are written.
162
187
  Use `--force` only when replacing all matching report artifacts is intentional:
163
188
 
164
189
  ```console
@@ -220,18 +245,48 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
220
245
  only the requested sample, plus stratum counts, in memory. Existing outputs
221
246
  require `--force`, and an input file is never overwritten.
222
247
 
248
+ ## Tabalyst Inspect
249
+
250
+ A JSON file can hold several arrays. **Tabalyst Inspect** reads a JSON, JSONL
251
+ or NDJSON file once, finds which array holds the records and writes its answer
252
+ in `<source>-inspect.json` beside the source:
253
+
254
+ ```console
255
+ tabalyst inspect orders.json
256
+ ```
257
+
258
+ ```text
259
+ Inspect: orders.json-inspect.json
260
+ Selection: $.customers[] (the only eligible collection)
261
+ ```
262
+
263
+ The last section of that file, `config`, holds the rules that `tabalyst scan`
264
+ and `tabalyst report` apply to the source: the collection
265
+ (`structure.dataset_path`), the flatten settings, the array mode and the error
266
+ policy. Edit it, then scan or report as usual. Running `tabalyst inspect` again
267
+ refreshes the detection and keeps your `config`; `--reset-config` replaces it.
268
+
269
+ Inspect is optional: `tabalyst scan` and `tabalyst report` inspect a JSON file
270
+ themselves when it has no Inspect file. When several arrays are equally
271
+ plausible, or none holds objects, they stop with exit code `2` before analyzing
272
+ anything and list the candidates; set `config.structure.dataset_path` in the
273
+ Inspect file, or pass `--collection`. See
274
+ [Inspect JSON and JSONL files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/inspect-json-files.md)
275
+ and the [Inspect format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/inspect-format.md).
276
+
223
277
  ## Tabalyst Scan
224
278
 
225
- Describe every field of a CSV or JSON file in one JSON document:
279
+ Describe every field of a CSV, JSON or JSONL file in one JSON document:
226
280
 
227
281
  ```console
228
282
  tabalyst scan customers.csv
229
283
  tabalyst scan orders.json --collection "$.customers[]"
284
+ tabalyst scan events.jsonl
230
285
  ```
231
286
 
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
287
+ Without `-o` or `-d`, the scan is a reusable `scan.json` under Tabalyst's local
288
+ storage directory (or `TABALYST_HOME`). The default workflow does not build a
289
+ DuckDB database. `-o` or `-d` writes
235
290
  a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
236
291
  a stream, with memory bounded by configurable limits, and records for each
237
292
  field its presence, native types, missing values, frequencies, exact
@@ -244,12 +299,12 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
244
299
  analyzed by several worker processes, with the same result; `--workers`
245
300
  chooses their number, `--workers 1` keeps one process.
246
301
 
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.
302
+ `tabalyst report customers.csv` reuses the verified stored scan when the
303
+ source content and requested scan settings are current, and so does a report on
304
+ a JSON or JSONL file. It creates a scan when none exists and atomically replaces
305
+ a stale one. The source check uses the SHA-256 of the whole content, and a scan
306
+ written by another version of Tabalyst is replaced. Existing DuckDB projects from 0.4.3 are left
307
+ untouched.
253
308
 
254
309
  Build the report from a standalone scan document without reading the source again:
255
310
 
@@ -260,8 +315,8 @@ tabalyst report --scan customers.scan.json
260
315
  Tabalyst refuses a scan whose source changed since it was written, or whose
261
316
  settings differ from the `scan` settings of `--config`.
262
317
 
263
- Inspect or clean disposable project query caches for every project, or for one
264
- CSV file:
318
+ Inspect or clean disposable query caches of existing DuckDB projects, for every
319
+ project or for one CSV file:
265
320
 
266
321
  ```console
267
322
  tabalyst cache info
@@ -270,7 +325,8 @@ tabalyst cache clean
270
325
  tabalyst cache clean customers.csv
271
326
  ```
272
327
 
273
- `cache clean` leaves project scans and databases in place.
328
+ `cache clean` leaves project scans and databases in place. Ordinary scans and
329
+ reports do not create these query caches.
274
330
 
275
331
  See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
276
332
  and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
@@ -320,14 +376,21 @@ sample = tabalyst.sample_csv(
320
376
 
321
377
  scan = tabalyst.scan("orders.json")
322
378
  scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
379
+
380
+ inspection = tabalyst.inspect("orders.json")
381
+ inspections = tabalyst.generate_inspections(["data/*.json", "logs/*.jsonl"])
323
382
  reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
324
383
  ```
325
384
 
326
385
  `analyze()` returns the JSON-serializable profile for one report.
327
386
  `generate_reports()` returns the complete batch plan, successes, and failures.
328
- `scan()` returns a scan result without writing anything; `generate_scans()`
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`
387
+ `scan()` returns a scan result without writing anything, at the level of the
388
+ engine: it does not read an Inspect file. `generate_scans()` writes one
389
+ standalone `.scan.json` document per source by default, and applies Inspect to
390
+ JSON and JSONL sources. Pass `project_storage=True` to use the command's
391
+ scan-only storage. `inspect()` writes the Inspect file of one JSON or JSONL
392
+ source and returns its path and document; `generate_inspections()` does it for
393
+ several sources. `from_scan=True`
331
394
  builds reports from standalone scan documents.
332
395
  Expected failures derive from `tabalyst.TabalystError`.
333
396
 
@@ -351,7 +414,8 @@ Configuration files are strict JSON. A minimal file is:
351
414
  ```
352
415
 
353
416
  Analysis settings, CSV reading included, go in the `scan` object, shared by
354
- `tabalyst report` and `tabalyst scan`. Top-level settings shape the report
417
+ `tabalyst report`, `tabalyst scan` and `tabalyst inspect`. The `config` of an
418
+ Inspect file outranks the `scan` object for the source it sits beside. Top-level settings shape the report
355
419
  presentation, and `csv` configures `tabalyst sample`. Explicit CLI or Python
356
420
  arguments override the configuration file, which overrides Tabalyst defaults.
357
421
  No configuration file is loaded unless it is passed with `--config`. See the
@@ -368,7 +432,9 @@ Reports, samples, and scans read files as a stream, with memory bounded by
368
432
  configurable limits, including duplicate-row detection
369
433
  (`scan.limits.max_tracked_records`). Reports and scans show the share of the file read;
370
434
  throughput estimates, recursive directory input, and parallel batch execution
371
- will require later work.
435
+ will require later work. Inspect reads a JSON or JSONL source once from start to
436
+ end, with little memory; on the synthetic benchmarks it took roughly a tenth of the
437
+ time of a scan, a ratio that depends on the data.
372
438
 
373
439
  ## Examples and development
374
440
 
@@ -386,6 +452,7 @@ Additional documentation:
386
452
 
387
453
  - [Architecture](https://github.com/loribel-labs/tabalyst/blob/main/docs/dev/architecture.md)
388
454
  - [Configuration](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
455
+ - [Inspect JSON and JSONL files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/inspect-json-files.md)
389
456
  - [Release procedure](https://github.com/loribel-labs/tabalyst/blob/main/RELEASING.md)
390
457
 
391
458
  Please report defects and feature requests through the
@@ -8,7 +8,8 @@ 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
11
+ **Tabalyst Scan** describes CSV, JSON and JSONL files in a scan document,
12
+ **Tabalyst Inspect** finds how to read a JSON or JSONL file, and
12
13
  **Tabalyst Sample** creates smaller CSV files.
13
14
 
14
15
  Tabalyst is in beta. Its interfaces may still change while
@@ -34,6 +35,7 @@ changes often: update it before each new test.
34
35
  | Several files, automatic names | `tabalyst report *.csv` | Beside each source |
35
36
  | Several files, one directory | `tabalyst report *.csv -d reports/` | The directory `reports/` |
36
37
  | One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
38
+ | One JSONL file | `tabalyst report events.jsonl` | `events.report.html` beside the source |
37
39
  | From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
38
40
 
39
41
  The simplest command keeps the source filename:
@@ -51,6 +53,20 @@ customers.json
51
53
  executions.json
52
54
  ```
53
55
 
56
+ Add `--details` to generate one self-contained HTML page per column:
57
+
58
+ ```console
59
+ tabalyst report customers.csv --details
60
+ ```
61
+
62
+ The **Columns** table then links to those pages. Each page has its own sidebar
63
+ and shows values, counts, detected formats and other analysis directly. The
64
+ subfolder takes the HTML filename without `.html`; for `-o report.html`, pages
65
+ are in `report/`. Names use `col-01-` and a shortened slug of the column name,
66
+ in source order. Details are off by default. `--no-details` states that choice
67
+ explicitly; with `--force`, it removes prior generated column pages while
68
+ preserving other files in the folder.
69
+
54
70
  Use `-o` to choose a different HTML filename for one source:
55
71
 
56
72
  ```console
@@ -59,16 +75,23 @@ tabalyst report customers.csv -o customer-analysis.html
59
75
 
60
76
  ### JSON files
61
77
 
62
- A JSON file gets a report too, named `<stem>.report.html` so its profile
63
- `<stem>.report.json` never replaces the source. Each collection of records,
64
- such as the `customers` array of `{"customers": [...]}`, is a dataset of the
65
- report, and nested fields are columns named by their path, such as
66
- `address.city`:
78
+ A JSON or JSONL file gets a report too, named `<stem>.report.html` so its
79
+ profile `<stem>.report.json` never replaces the source. The report analyzes one
80
+ collection of records, such as the `customers` array of
81
+ `{"customers": [...]}`, chosen by Tabalyst Inspect, and nested fields are
82
+ columns named by their path, such as `address.city`:
67
83
 
68
84
  ```console
69
85
  tabalyst report orders.json
86
+ tabalyst report events.jsonl
70
87
  ```
71
88
 
89
+ In a JSONL file (`.jsonl` or `.ndjson`), each line is a record. A line that is
90
+ not valid JSON or not an object is excluded and counted, and the report is
91
+ partial. When a JSON file holds several arrays that are equally plausible,
92
+ Tabalyst stops and lists them instead of choosing: see
93
+ [Tabalyst Inspect](#tabalyst-inspect).
94
+
72
95
  ### Multiple files
73
96
 
74
97
  Report several CSV files at once:
@@ -121,8 +144,10 @@ instead.
121
144
 
122
145
  ### Safe batch behavior
123
146
 
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.
147
+ Before processing begins, Tabalyst resolves every input and planned main HTML
148
+ and JSON output. It stops the entire batch if those names collide or if an
149
+ output already exists. With `--details`, each column page is checked after its
150
+ profile has been built, before that report's files are written.
126
151
  Use `--force` only when replacing all matching report artifacts is intentional:
127
152
 
128
153
  ```console
@@ -184,18 +209,48 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
184
209
  only the requested sample, plus stratum counts, in memory. Existing outputs
185
210
  require `--force`, and an input file is never overwritten.
186
211
 
212
+ ## Tabalyst Inspect
213
+
214
+ A JSON file can hold several arrays. **Tabalyst Inspect** reads a JSON, JSONL
215
+ or NDJSON file once, finds which array holds the records and writes its answer
216
+ in `<source>-inspect.json` beside the source:
217
+
218
+ ```console
219
+ tabalyst inspect orders.json
220
+ ```
221
+
222
+ ```text
223
+ Inspect: orders.json-inspect.json
224
+ Selection: $.customers[] (the only eligible collection)
225
+ ```
226
+
227
+ The last section of that file, `config`, holds the rules that `tabalyst scan`
228
+ and `tabalyst report` apply to the source: the collection
229
+ (`structure.dataset_path`), the flatten settings, the array mode and the error
230
+ policy. Edit it, then scan or report as usual. Running `tabalyst inspect` again
231
+ refreshes the detection and keeps your `config`; `--reset-config` replaces it.
232
+
233
+ Inspect is optional: `tabalyst scan` and `tabalyst report` inspect a JSON file
234
+ themselves when it has no Inspect file. When several arrays are equally
235
+ plausible, or none holds objects, they stop with exit code `2` before analyzing
236
+ anything and list the candidates; set `config.structure.dataset_path` in the
237
+ Inspect file, or pass `--collection`. See
238
+ [Inspect JSON and JSONL files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/inspect-json-files.md)
239
+ and the [Inspect format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/inspect-format.md).
240
+
187
241
  ## Tabalyst Scan
188
242
 
189
- Describe every field of a CSV or JSON file in one JSON document:
243
+ Describe every field of a CSV, JSON or JSONL file in one JSON document:
190
244
 
191
245
  ```console
192
246
  tabalyst scan customers.csv
193
247
  tabalyst scan orders.json --collection "$.customers[]"
248
+ tabalyst scan events.jsonl
194
249
  ```
195
250
 
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
251
+ Without `-o` or `-d`, the scan is a reusable `scan.json` under Tabalyst's local
252
+ storage directory (or `TABALYST_HOME`). The default workflow does not build a
253
+ DuckDB database. `-o` or `-d` writes
199
254
  a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
200
255
  a stream, with memory bounded by configurable limits, and records for each
201
256
  field its presence, native types, missing values, frequencies, exact
@@ -208,12 +263,12 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
208
263
  analyzed by several worker processes, with the same result; `--workers`
209
264
  chooses their number, `--workers 1` keeps one process.
210
265
 
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.
266
+ `tabalyst report customers.csv` reuses the verified stored scan when the
267
+ source content and requested scan settings are current, and so does a report on
268
+ a JSON or JSONL file. It creates a scan when none exists and atomically replaces
269
+ a stale one. The source check uses the SHA-256 of the whole content, and a scan
270
+ written by another version of Tabalyst is replaced. Existing DuckDB projects from 0.4.3 are left
271
+ untouched.
217
272
 
218
273
  Build the report from a standalone scan document without reading the source again:
219
274
 
@@ -224,8 +279,8 @@ tabalyst report --scan customers.scan.json
224
279
  Tabalyst refuses a scan whose source changed since it was written, or whose
225
280
  settings differ from the `scan` settings of `--config`.
226
281
 
227
- Inspect or clean disposable project query caches for every project, or for one
228
- CSV file:
282
+ Inspect or clean disposable query caches of existing DuckDB projects, for every
283
+ project or for one CSV file:
229
284
 
230
285
  ```console
231
286
  tabalyst cache info
@@ -234,7 +289,8 @@ tabalyst cache clean
234
289
  tabalyst cache clean customers.csv
235
290
  ```
236
291
 
237
- `cache clean` leaves project scans and databases in place.
292
+ `cache clean` leaves project scans and databases in place. Ordinary scans and
293
+ reports do not create these query caches.
238
294
 
239
295
  See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
240
296
  and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
@@ -284,14 +340,21 @@ sample = tabalyst.sample_csv(
284
340
 
285
341
  scan = tabalyst.scan("orders.json")
286
342
  scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
343
+
344
+ inspection = tabalyst.inspect("orders.json")
345
+ inspections = tabalyst.generate_inspections(["data/*.json", "logs/*.jsonl"])
287
346
  reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
288
347
  ```
289
348
 
290
349
  `analyze()` returns the JSON-serializable profile for one report.
291
350
  `generate_reports()` returns the complete batch plan, successes, and failures.
292
- `scan()` returns a scan result without writing anything; `generate_scans()`
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`
351
+ `scan()` returns a scan result without writing anything, at the level of the
352
+ engine: it does not read an Inspect file. `generate_scans()` writes one
353
+ standalone `.scan.json` document per source by default, and applies Inspect to
354
+ JSON and JSONL sources. Pass `project_storage=True` to use the command's
355
+ scan-only storage. `inspect()` writes the Inspect file of one JSON or JSONL
356
+ source and returns its path and document; `generate_inspections()` does it for
357
+ several sources. `from_scan=True`
295
358
  builds reports from standalone scan documents.
296
359
  Expected failures derive from `tabalyst.TabalystError`.
297
360
 
@@ -315,7 +378,8 @@ Configuration files are strict JSON. A minimal file is:
315
378
  ```
316
379
 
317
380
  Analysis settings, CSV reading included, go in the `scan` object, shared by
318
- `tabalyst report` and `tabalyst scan`. Top-level settings shape the report
381
+ `tabalyst report`, `tabalyst scan` and `tabalyst inspect`. The `config` of an
382
+ Inspect file outranks the `scan` object for the source it sits beside. Top-level settings shape the report
319
383
  presentation, and `csv` configures `tabalyst sample`. Explicit CLI or Python
320
384
  arguments override the configuration file, which overrides Tabalyst defaults.
321
385
  No configuration file is loaded unless it is passed with `--config`. See the
@@ -332,7 +396,9 @@ Reports, samples, and scans read files as a stream, with memory bounded by
332
396
  configurable limits, including duplicate-row detection
333
397
  (`scan.limits.max_tracked_records`). Reports and scans show the share of the file read;
334
398
  throughput estimates, recursive directory input, and parallel batch execution
335
- will require later work.
399
+ will require later work. Inspect reads a JSON or JSONL source once from start to
400
+ end, with little memory; on the synthetic benchmarks it took roughly a tenth of the
401
+ time of a scan, a ratio that depends on the data.
336
402
 
337
403
  ## Examples and development
338
404
 
@@ -350,6 +416,7 @@ Additional documentation:
350
416
 
351
417
  - [Architecture](https://github.com/loribel-labs/tabalyst/blob/main/docs/dev/architecture.md)
352
418
  - [Configuration](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/configuration.md)
419
+ - [Inspect JSON and JSONL files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/inspect-json-files.md)
353
420
  - [Release procedure](https://github.com/loribel-labs/tabalyst/blob/main/RELEASING.md)
354
421
 
355
422
  Please report defects and feature requests through the
@@ -9,7 +9,8 @@ examples/
9
9
  |-- input/
10
10
  | |-- basic.csv
11
11
  | |-- insurance-customers.csv
12
- | `-- orders.json
12
+ | |-- orders.json
13
+ | `-- web-events.jsonl
13
14
  `-- output/
14
15
  |-- basic/
15
16
  | |-- report.html
@@ -18,8 +19,13 @@ examples/
18
19
  |-- insurance-customers/
19
20
  | |-- report.html
20
21
  | |-- report.json
22
+ | |-- report/ (34 standalone column pages with --details)
21
23
  | `-- executions.json
22
- `-- orders/
24
+ |-- orders/
25
+ | |-- report.html
26
+ | |-- report.json
27
+ | `-- executions.json
28
+ `-- web-events/
23
29
  |-- report.html
24
30
  |-- report.json
25
31
  `-- executions.json
@@ -29,19 +35,27 @@ examples/
29
35
  synthetic customer and contract records across 34 columns. Its people, contact
30
36
  details, and contracts are fictional; email addresses use reserved `.example`
31
37
  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[]`.
38
+ addresses, tags and orders. Its report has one dataset, `$.customers[]`, the only
39
+ collection of arrays of objects that Tabalyst Inspect finds in it.
40
+ `web-events.jsonl` is a synthetic web analytics log in JSON Lines: 300 events with
41
+ nested `user` and `device` objects, tags arrays, null values and an optional
42
+ field that appears late, plus three bad lines (a truncated record, a line that
43
+ is not JSON and a JSON array instead of an object). Its report is partial on
44
+ purpose: the three lines are excluded and counted, the 300 events are analyzed.
45
+ Its people and identifiers are fictional.
34
46
 
35
47
  From the repository root, regenerate the outputs with:
36
48
 
37
49
  ```console
38
50
  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
51
+ tabalyst report examples/input/insurance-customers.csv -o examples/output/insurance-customers/report.html --config examples/config.json --details --force
40
52
  tabalyst report examples/input/orders.json -o examples/output/orders/report.html --config examples/config.json --force
53
+ tabalyst report examples/input/web-events.jsonl -o examples/output/web-events/report.html --config examples/config.json --force
41
54
  ```
42
55
 
43
56
  Every output folder is independent and contains its own HTML report, canonical
44
- JSON profile, and cumulative `executions.json` history. Delete an output folder
57
+ JSON profile and cumulative `executions.json` history. The insurance example
58
+ also has standalone HTML pages under `report/`. Delete an output folder
45
59
  before regenerating when a fresh one-entry execution history is required.
46
60
 
47
61
  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.5.0"
8
8
  description = "Open-source toolkit for understanding and working with unfamiliar data."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -69,6 +69,7 @@ testpaths = ["tests"]
69
69
  pythonpath = ["src"]
70
70
  markers = [
71
71
  "lot(name): Tabalyst Scan implementation lot that provides the tested behavior",
72
+ "inspect_lot(name): Tabalyst Inspect implementation lot that provides the tested behavior",
72
73
  ]
73
74
 
74
75
  [tool.ruff]
@@ -2,6 +2,9 @@
2
2
 
3
3
  from tabalyst._version import __version__
4
4
  from tabalyst.errors import ConfigurationError, InputError, ReportError, TabalystError
5
+ from tabalyst.inspect_service import InspectResult as InspectResult
6
+ from tabalyst.inspect_service import generate_inspections as generate_inspections
7
+ from tabalyst.inspect_service import inspect as inspect
5
8
  from tabalyst.report_config import ReportConfig as ReportConfig
6
9
  from tabalyst.report_service import generate_reports as generate_reports
7
10
  from tabalyst.reporting import render_report as render_report
@@ -20,15 +23,18 @@ from tabalyst.service import analyze_scan as analyze_scan
20
23
  __all__ = [
21
24
  "ConfigurationError",
22
25
  "InputError",
26
+ "InspectResult",
23
27
  "ReportError",
24
28
  "ScanConfig",
25
29
  "ScanResult",
26
30
  "TabalystError",
27
31
  "__version__",
28
32
  "analyze",
33
+ "generate_inspections",
29
34
  "generate_reports",
30
35
  "generate_samples",
31
36
  "generate_scans",
37
+ "inspect",
32
38
  "sample_csv",
33
39
  "scan",
34
40
  ]
@@ -13,6 +13,15 @@ from pathlib import Path
13
13
 
14
14
  from tabalyst.errors import ConfigurationError, InputError, ReportError
15
15
 
16
+ # The visible Inspect file of a source is its full name plus this suffix
17
+ # (design inspect 12.1). It is never a source itself.
18
+ INSPECT_SUFFIX = "-inspect.json"
19
+
20
+
21
+ def is_inspect_name(name: str) -> bool:
22
+ """Whether a file name is that of an Inspect file."""
23
+ return name.endswith(INSPECT_SUFFIX)
24
+
16
25
 
17
26
  def path_key(path: Path) -> str:
18
27
  """Identity of a path for comparisons, whatever its spelling and case."""
@@ -20,7 +29,11 @@ def path_key(path: Path) -> str:
20
29
 
21
30
 
22
31
  def resolve_input_specs(input_specs: Sequence[str | Path]) -> list[Path]:
23
- """Resolve explicit files and shell-independent, non-recursive glob patterns."""
32
+ """Resolve explicit files and shell-independent, non-recursive glob patterns.
33
+
34
+ A pattern skips the Inspect files it matches; an explicit path to one is
35
+ refused (design inspect 12.1).
36
+ """
24
37
  if not input_specs:
25
38
  raise ConfigurationError("At least one input file is required.")
26
39
 
@@ -34,7 +47,11 @@ def resolve_input_specs(input_specs: Sequence[str | Path]) -> list[Path]:
34
47
  )
35
48
  if glob.has_magic(text):
36
49
  matches = sorted(
37
- (Path(match) for match in glob.glob(text) if Path(match).is_file()),
50
+ (
51
+ Path(match)
52
+ for match in glob.glob(text)
53
+ if Path(match).is_file() and not is_inspect_name(Path(match).name)
54
+ ),
38
55
  key=path_key,
39
56
  )
40
57
  if not matches:
@@ -45,6 +62,11 @@ def resolve_input_specs(input_specs: Sequence[str | Path]) -> list[Path]:
45
62
  raise InputError(f"Input file does not exist: {path}")
46
63
  if not path.is_file():
47
64
  raise InputError(f"Input path is not a file: {path}")
65
+ if is_inspect_name(path.name):
66
+ raise ConfigurationError(
67
+ f"{path} is an Inspect file, not a source. Give the source it "
68
+ "describes; edit the Inspect file to change how it is read."
69
+ )
48
70
  matches = [path]
49
71
 
50
72
  for path in matches:
@@ -6,6 +6,7 @@ import typer
6
6
 
7
7
  from tabalyst import __version__
8
8
  from tabalyst.cli.cache import cache_app
9
+ from tabalyst.cli.inspect import inspect_command
9
10
  from tabalyst.cli.report import report_command
10
11
  from tabalyst.cli.sample import sample_command
11
12
  from tabalyst.cli.scan import scan_command
@@ -39,6 +40,7 @@ def root(
39
40
  """Understand, explore, clean, and validate structured data."""
40
41
 
41
42
 
43
+ app.command("inspect")(inspect_command)
42
44
  app.command("report")(report_command)
43
45
  app.command("sample")(sample_command)
44
46
  app.command("scan")(scan_command)
@@ -46,5 +48,10 @@ app.add_typer(cache_app, name="cache")
46
48
 
47
49
 
48
50
  def main() -> None:
49
- """Run the Tabalyst command-line application."""
50
- app()
51
+ """Run the Tabalyst command-line application.
52
+
53
+ Windows wildcard expansion is left to Tabalyst: its own pattern resolver
54
+ skips the Inspect files that a shell-expanded ``*.json`` would pass as
55
+ explicit paths.
56
+ """
57
+ app(windows_expand_args=False)