tabalyst 0.6.0__tar.gz → 0.6.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.
- {tabalyst-0.6.0/src/tabalyst.egg-info → tabalyst-0.6.1}/PKG-INFO +39 -6
- {tabalyst-0.6.0 → tabalyst-0.6.1}/README.md +38 -5
- {tabalyst-0.6.0 → tabalyst-0.6.1}/pyproject.toml +1 -1
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/batch.py +11 -3
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/inspect.py +6 -9
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/report.py +26 -5
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/scan.py +8 -2
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/terminal.py +18 -14
- tabalyst-0.6.1/src/tabalyst/inspector/choices.py +114 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/resolution.py +39 -4
- tabalyst-0.6.1/src/tabalyst/option_notices.py +54 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/report_service.py +185 -4
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scan_service.py +17 -1
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/static/theme.css +1 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/templates/column.html +2 -2
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/templates/report.html +2 -2
- {tabalyst-0.6.0 → tabalyst-0.6.1/src/tabalyst.egg-info}/PKG-INFO +39 -6
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/SOURCES.txt +4 -0
- tabalyst-0.6.1/tests/test_all_collections.py +312 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_cli.py +2 -2
- tabalyst-0.6.1/tests/test_option_notices.py +89 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_reporting.py +4 -3
- {tabalyst-0.6.0 → tabalyst-0.6.1}/LICENSE +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/MANIFEST.in +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/README.md +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/config.json +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/input/basic.csv +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/setup.cfg +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cache_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/app.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/cache.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/sample.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/config.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/execution_log.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspect_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/build.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/detect.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/parameters.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/build.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/detect.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/parameters.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/models.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/persistence.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/models.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/progress.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/project_open_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/project_scan_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_bounded_values.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_cache.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_codec.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_generation.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_locks.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_publication.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_queries.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_query_budget.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_recovery.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_retention.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_schema.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_session.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_staging.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_sync.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_validation.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/catalog.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/freshness.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/identity.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/index.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/location.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/models.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/store.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/report_config.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/report_profile.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/reporting.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/sampling.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/sampling_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scan_reuse.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/api.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/config.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/amount.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/base.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/boolean.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/currency.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/email.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/ip.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/names.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/number.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/pattern.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/percentage.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/phone.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/postal.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/quantity.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/registry.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/shape.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/temporal.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/url.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/uuid.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/diagnostics.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/engine.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/exposure.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/field.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/identity.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/measures.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/models.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/normalization.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/observations.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/paths.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/__init__.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/base.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/excel_common.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/excel_reader.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/excel_table.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/json_common.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/json_reader.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/jsonl_reader.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/records.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/structure.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/technical.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/values.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/workers.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/shared_scan_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/static/report.js +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/requires.txt +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/top_level.txt +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_license_headers.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_cache.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_database.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_generations.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_identity.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_index.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_location.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_open_service.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_queries.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_rescan.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_scan.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_sessions.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_store.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_public_api.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_sampling.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scan_command.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_amounts.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_batches.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_config.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_csv.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_email_url.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_exposure.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_paths.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_phone.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_postal.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_uuid_ip.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_shared_scan_cli.py +0 -0
- {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_source_freshness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabalyst
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Open-source toolkit for understanding and working with unfamiliar data.
|
|
5
5
|
Author: Gregory Borelli
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -75,6 +75,7 @@ changes often: update it before each new test.
|
|
|
75
75
|
| One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
|
|
76
76
|
| One JSONL file | `tabalyst report events.jsonl` | `events.report.html` beside the source |
|
|
77
77
|
| One Excel workbook | `tabalyst report sales.xlsx` | `sales.report.html` beside the source |
|
|
78
|
+
| Every table of a workbook | `tabalyst report shop.xlsx --all-collections` | `shop.costs.html`, `shop.sales.html`, ... beside the source |
|
|
78
79
|
| From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
|
|
79
80
|
|
|
80
81
|
The simplest command keeps the source filename:
|
|
@@ -143,9 +144,17 @@ tabalyst report sales.xlsx
|
|
|
143
144
|
|
|
144
145
|
Cells keep their Excel type, and dates are read as dates. A workbook with one
|
|
145
146
|
table needs nothing else. When several sheets or tables are equally plausible,
|
|
146
|
-
Tabalyst stops and
|
|
147
|
-
`
|
|
148
|
-
|
|
147
|
+
Tabalyst stops and prints one command per table to copy and run, such as
|
|
148
|
+
`tabalyst report shop.xlsx --collection Costs` (or `Sales.Orders` for a named
|
|
149
|
+
table), or see [Tabalyst Inspect](#tabalyst-inspect). To report every table at
|
|
150
|
+
once, use `--all-collections`:
|
|
151
|
+
|
|
152
|
+
```console
|
|
153
|
+
tabalyst report shop.xlsx --all-collections -d reports
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This writes `shop.costs.html` and `shop.costs.json` for each table, named after
|
|
157
|
+
the file and the sheet. Older `.xls` files are not read: save them as `.xlsx`.
|
|
149
158
|
|
|
150
159
|
### Multiple files
|
|
151
160
|
|
|
@@ -417,7 +426,9 @@ scan = tabalyst.scan("orders.json")
|
|
|
417
426
|
scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
|
|
418
427
|
|
|
419
428
|
inspection = tabalyst.inspect("orders.json")
|
|
420
|
-
inspections = tabalyst.generate_inspections(
|
|
429
|
+
inspections = tabalyst.generate_inspections(
|
|
430
|
+
["data/*.json", "logs/*.jsonl", "*.xlsx"]
|
|
431
|
+
)
|
|
421
432
|
reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
422
433
|
```
|
|
423
434
|
|
|
@@ -503,8 +514,30 @@ Additional documentation:
|
|
|
503
514
|
Please report defects and feature requests through the
|
|
504
515
|
[GitHub issue tracker](https://github.com/loribel-labs/tabalyst/issues).
|
|
505
516
|
|
|
517
|
+
## For AI assistants
|
|
518
|
+
|
|
519
|
+
To get help from an AI assistant, paste this prompt into a chat that can read
|
|
520
|
+
web pages:
|
|
521
|
+
|
|
522
|
+
```text
|
|
523
|
+
Read the complete Tabalyst documentation at
|
|
524
|
+
https://docs.tabalyst.com/llms-full.txt, then help me use Tabalyst well.
|
|
525
|
+
Ask me what my data looks like and what I want to learn from it, then suggest
|
|
526
|
+
the right commands and options. Base your answers only on that documentation.
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
If the assistant cannot open links, paste the content of the file instead.
|
|
530
|
+
|
|
531
|
+
The documentation site publishes plain-text versions of its pages:
|
|
532
|
+
[llms.txt](https://docs.tabalyst.com/llms.txt) lists every page with a
|
|
533
|
+
one-line description, and
|
|
534
|
+
[llms-full.txt](https://docs.tabalyst.com/llms-full.txt) holds the whole
|
|
535
|
+
documentation in one file. Every page is also available as Markdown by adding
|
|
536
|
+
`.md` to its URL, for example
|
|
537
|
+
[report.md](https://docs.tabalyst.com/report.md).
|
|
538
|
+
|
|
506
539
|
## License
|
|
507
540
|
|
|
508
541
|
Tabalyst is released under the [Mozilla Public License 2.0](LICENSE).
|
|
509
542
|
|
|
510
|
-
Created by Gregory Borelli
|
|
543
|
+
Created by Gregory Borelli. Open source project by Loribel Labs.
|
|
@@ -37,6 +37,7 @@ changes often: update it before each new test.
|
|
|
37
37
|
| One JSON file | `tabalyst report data.json` | `data.report.html` beside the source |
|
|
38
38
|
| One JSONL file | `tabalyst report events.jsonl` | `events.report.html` beside the source |
|
|
39
39
|
| One Excel workbook | `tabalyst report sales.xlsx` | `sales.report.html` beside the source |
|
|
40
|
+
| Every table of a workbook | `tabalyst report shop.xlsx --all-collections` | `shop.costs.html`, `shop.sales.html`, ... beside the source |
|
|
40
41
|
| From a scan document | `tabalyst report --scan data.scan.json` | `data.html` beside the scan |
|
|
41
42
|
|
|
42
43
|
The simplest command keeps the source filename:
|
|
@@ -105,9 +106,17 @@ tabalyst report sales.xlsx
|
|
|
105
106
|
|
|
106
107
|
Cells keep their Excel type, and dates are read as dates. A workbook with one
|
|
107
108
|
table needs nothing else. When several sheets or tables are equally plausible,
|
|
108
|
-
Tabalyst stops and
|
|
109
|
-
`
|
|
110
|
-
|
|
109
|
+
Tabalyst stops and prints one command per table to copy and run, such as
|
|
110
|
+
`tabalyst report shop.xlsx --collection Costs` (or `Sales.Orders` for a named
|
|
111
|
+
table), or see [Tabalyst Inspect](#tabalyst-inspect). To report every table at
|
|
112
|
+
once, use `--all-collections`:
|
|
113
|
+
|
|
114
|
+
```console
|
|
115
|
+
tabalyst report shop.xlsx --all-collections -d reports
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This writes `shop.costs.html` and `shop.costs.json` for each table, named after
|
|
119
|
+
the file and the sheet. Older `.xls` files are not read: save them as `.xlsx`.
|
|
111
120
|
|
|
112
121
|
### Multiple files
|
|
113
122
|
|
|
@@ -379,7 +388,9 @@ scan = tabalyst.scan("orders.json")
|
|
|
379
388
|
scans = tabalyst.generate_scans(["data/*.json"], output_dir="scans")
|
|
380
389
|
|
|
381
390
|
inspection = tabalyst.inspect("orders.json")
|
|
382
|
-
inspections = tabalyst.generate_inspections(
|
|
391
|
+
inspections = tabalyst.generate_inspections(
|
|
392
|
+
["data/*.json", "logs/*.jsonl", "*.xlsx"]
|
|
393
|
+
)
|
|
383
394
|
reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
384
395
|
```
|
|
385
396
|
|
|
@@ -465,8 +476,30 @@ Additional documentation:
|
|
|
465
476
|
Please report defects and feature requests through the
|
|
466
477
|
[GitHub issue tracker](https://github.com/loribel-labs/tabalyst/issues).
|
|
467
478
|
|
|
479
|
+
## For AI assistants
|
|
480
|
+
|
|
481
|
+
To get help from an AI assistant, paste this prompt into a chat that can read
|
|
482
|
+
web pages:
|
|
483
|
+
|
|
484
|
+
```text
|
|
485
|
+
Read the complete Tabalyst documentation at
|
|
486
|
+
https://docs.tabalyst.com/llms-full.txt, then help me use Tabalyst well.
|
|
487
|
+
Ask me what my data looks like and what I want to learn from it, then suggest
|
|
488
|
+
the right commands and options. Base your answers only on that documentation.
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
If the assistant cannot open links, paste the content of the file instead.
|
|
492
|
+
|
|
493
|
+
The documentation site publishes plain-text versions of its pages:
|
|
494
|
+
[llms.txt](https://docs.tabalyst.com/llms.txt) lists every page with a
|
|
495
|
+
one-line description, and
|
|
496
|
+
[llms-full.txt](https://docs.tabalyst.com/llms-full.txt) holds the whole
|
|
497
|
+
documentation in one file. Every page is also available as Markdown by adding
|
|
498
|
+
`.md` to its URL, for example
|
|
499
|
+
[report.md](https://docs.tabalyst.com/report.md).
|
|
500
|
+
|
|
468
501
|
## License
|
|
469
502
|
|
|
470
503
|
Tabalyst is released under the [Mozilla Public License 2.0](LICENSE).
|
|
471
504
|
|
|
472
|
-
Created by Gregory Borelli
|
|
505
|
+
Created by Gregory Borelli. Open source project by Loribel Labs.
|
|
@@ -90,6 +90,16 @@ def resolve_input_specs(input_specs: Sequence[str | Path]) -> list[Path]:
|
|
|
90
90
|
return sources
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def output_directory(output_dir: str | Path | None) -> Path | None:
|
|
94
|
+
"""``--output-dir`` as a path, refused when it is an existing file."""
|
|
95
|
+
if output_dir is None:
|
|
96
|
+
return None
|
|
97
|
+
destination = Path(output_dir)
|
|
98
|
+
if destination.exists() and not destination.is_dir():
|
|
99
|
+
raise ReportError(f"Output directory path is a file: {destination}")
|
|
100
|
+
return destination
|
|
101
|
+
|
|
102
|
+
|
|
93
103
|
def plan_outputs(
|
|
94
104
|
input_specs: Sequence[str | Path],
|
|
95
105
|
*,
|
|
@@ -108,9 +118,7 @@ def plan_outputs(
|
|
|
108
118
|
if output is not None and len(sources) != 1:
|
|
109
119
|
raise ConfigurationError("--output can only be used with one input file.")
|
|
110
120
|
|
|
111
|
-
destination =
|
|
112
|
-
if destination is not None and destination.exists() and not destination.is_dir():
|
|
113
|
-
raise ReportError(f"Output directory path is a file: {destination}")
|
|
121
|
+
destination = output_directory(output_dir)
|
|
114
122
|
return [
|
|
115
123
|
(
|
|
116
124
|
source,
|
|
@@ -17,6 +17,7 @@ from tabalyst.inspect_service import (
|
|
|
17
17
|
InspectResult,
|
|
18
18
|
generate_inspections,
|
|
19
19
|
)
|
|
20
|
+
from tabalyst.inspector.choices import choice_lines
|
|
20
21
|
|
|
21
22
|
_BASES = {
|
|
22
23
|
"root_array": "the root array",
|
|
@@ -50,16 +51,12 @@ def _choices(success: InspectResult) -> list[str]:
|
|
|
50
51
|
detection = success.document.detection
|
|
51
52
|
if detection.selection.basis != "ambiguous":
|
|
52
53
|
return []
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
source
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
f" tabalyst report {source} --collection '{candidate.path}'"
|
|
59
|
-
for candidate in detection.candidates
|
|
60
|
-
if candidate.eligible
|
|
54
|
+
return choice_lines(
|
|
55
|
+
"report",
|
|
56
|
+
success.source,
|
|
57
|
+
[candidate.path for candidate in detection.candidates if candidate.eligible],
|
|
58
|
+
noun="table" if success.document.inspect.kind == "excel" else "collection",
|
|
61
59
|
)
|
|
62
|
-
return lines
|
|
63
60
|
|
|
64
61
|
|
|
65
62
|
def _print_success(success: InspectResult, *, verbose: bool) -> None:
|
|
@@ -16,6 +16,7 @@ from tabalyst.cli.terminal import (
|
|
|
16
16
|
batch_exit_code,
|
|
17
17
|
collection_paths,
|
|
18
18
|
error_exit_code,
|
|
19
|
+
error_text,
|
|
19
20
|
)
|
|
20
21
|
from tabalyst.errors import TabalystError
|
|
21
22
|
from tabalyst.report_service import BatchReportResult, ReportSuccess, generate_reports
|
|
@@ -70,6 +71,8 @@ def _print_batch_result(
|
|
|
70
71
|
details.append(f"delimiter {source['delimiter']!r}")
|
|
71
72
|
typer.echo(" " + " | ".join(details), err=True)
|
|
72
73
|
|
|
74
|
+
for source, warning in batch.plan.warnings:
|
|
75
|
+
typer.echo(f"Warning [{source}]: {warning}", err=True)
|
|
73
76
|
for success in batch.successes:
|
|
74
77
|
for warning in success.warnings:
|
|
75
78
|
typer.echo(f"Warning [{success.job.source}]: {warning}", err=True)
|
|
@@ -81,7 +84,10 @@ def _print_batch_result(
|
|
|
81
84
|
err=True,
|
|
82
85
|
)
|
|
83
86
|
for failure in batch.failures:
|
|
84
|
-
typer.echo(
|
|
87
|
+
typer.echo(
|
|
88
|
+
f"Error [{failure.job.source}]: {error_text(failure.error, 'report')}",
|
|
89
|
+
err=True,
|
|
90
|
+
)
|
|
85
91
|
|
|
86
92
|
if len(batch.plan.jobs) > 1 and (not quiet or batch.failures):
|
|
87
93
|
typer.echo(
|
|
@@ -96,8 +102,8 @@ def report_command(
|
|
|
96
102
|
typer.Argument(
|
|
97
103
|
metavar="INPUT...",
|
|
98
104
|
help=(
|
|
99
|
-
"One or more CSV
|
|
100
|
-
"or non-recursive glob patterns."
|
|
105
|
+
"One or more CSV, JSON, JSONL or Excel (.xlsx, .xlsm) files, or "
|
|
106
|
+
"scan documents with --scan, or non-recursive glob patterns."
|
|
101
107
|
),
|
|
102
108
|
),
|
|
103
109
|
],
|
|
@@ -114,7 +120,10 @@ def report_command(
|
|
|
114
120
|
typer.Option(
|
|
115
121
|
"--output-dir",
|
|
116
122
|
"-d",
|
|
117
|
-
help=
|
|
123
|
+
help=(
|
|
124
|
+
"Directory for reports named after their source files; "
|
|
125
|
+
"required by --all-collections with several inputs."
|
|
126
|
+
),
|
|
118
127
|
),
|
|
119
128
|
] = None,
|
|
120
129
|
delimiter: Annotated[
|
|
@@ -133,6 +142,17 @@ def report_command(
|
|
|
133
142
|
list[str] | None,
|
|
134
143
|
typer.Option("--collection", help=COLLECTION_HELP),
|
|
135
144
|
] = None,
|
|
145
|
+
all_collections: Annotated[
|
|
146
|
+
bool,
|
|
147
|
+
typer.Option(
|
|
148
|
+
"--all-collections",
|
|
149
|
+
help=(
|
|
150
|
+
"Report every visible collection of a JSON file or Excel "
|
|
151
|
+
"workbook, as <name>.<collection>.html and .json; needs -d "
|
|
152
|
+
"with several inputs."
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
] = False,
|
|
136
156
|
from_scan: Annotated[
|
|
137
157
|
bool,
|
|
138
158
|
typer.Option(
|
|
@@ -202,9 +222,10 @@ def report_command(
|
|
|
202
222
|
on_progress=progress,
|
|
203
223
|
from_scan=from_scan,
|
|
204
224
|
workers=workers,
|
|
225
|
+
all_collections=all_collections,
|
|
205
226
|
)
|
|
206
227
|
except TabalystError as exc:
|
|
207
|
-
typer.echo(f"Error: {exc}", err=True)
|
|
228
|
+
typer.echo(f"Error: {error_text(exc, 'report')}", err=True)
|
|
208
229
|
raise typer.Exit(error_exit_code(exc)) from exc
|
|
209
230
|
|
|
210
231
|
_print_batch_result(batch, quiet=quiet, verbose=verbose)
|
|
@@ -16,6 +16,7 @@ from tabalyst.cli.terminal import (
|
|
|
16
16
|
batch_exit_code,
|
|
17
17
|
collection_paths,
|
|
18
18
|
error_exit_code,
|
|
19
|
+
error_text,
|
|
19
20
|
)
|
|
20
21
|
from tabalyst.errors import TabalystError
|
|
21
22
|
from tabalyst.scan_service import BatchScanResult, ScanSuccess, generate_scans
|
|
@@ -86,13 +87,18 @@ def _print_batch_result(
|
|
|
86
87
|
err=True,
|
|
87
88
|
)
|
|
88
89
|
|
|
90
|
+
for source, warning in batch.plan.warnings:
|
|
91
|
+
typer.echo(f"Warning [{source}]: {warning}", err=True)
|
|
89
92
|
for success in batch.successes:
|
|
90
93
|
if success.result.status == "partial":
|
|
91
94
|
_print_partial(success)
|
|
92
95
|
for warning in success.warnings:
|
|
93
96
|
typer.echo(f"Warning [{success.job.source}]: {warning}", err=True)
|
|
94
97
|
for failure in batch.failures:
|
|
95
|
-
typer.echo(
|
|
98
|
+
typer.echo(
|
|
99
|
+
f"Error [{failure.job.source}]: {error_text(failure.error, 'scan')}",
|
|
100
|
+
err=True,
|
|
101
|
+
)
|
|
96
102
|
|
|
97
103
|
if len(batch.plan.jobs) > 1 and (not quiet or batch.failures):
|
|
98
104
|
typer.echo(
|
|
@@ -198,7 +204,7 @@ def scan_command(
|
|
|
198
204
|
project_storage=True,
|
|
199
205
|
)
|
|
200
206
|
except TabalystError as exc:
|
|
201
|
-
typer.echo(f"Error: {exc}", err=True)
|
|
207
|
+
typer.echo(f"Error: {error_text(exc, 'scan')}", err=True)
|
|
202
208
|
raise typer.Exit(error_exit_code(exc)) from exc
|
|
203
209
|
|
|
204
210
|
_print_batch_result(batch, quiet=quiet, verbose=verbose)
|
|
@@ -10,8 +10,13 @@ from typing import ClassVar
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from tabalyst.errors import ConfigurationError, InputError, TabalystError
|
|
13
|
+
from tabalyst.inspector.choices import (
|
|
14
|
+
UndecidedCollection,
|
|
15
|
+
choice_lines,
|
|
16
|
+
expand_collection,
|
|
17
|
+
)
|
|
13
18
|
from tabalyst.progress import ProgressEvent, ProgressPhase
|
|
14
|
-
from tabalyst.scanner.
|
|
19
|
+
from tabalyst.scanner.readers.excel_common import EXCEL_SUFFIXES
|
|
15
20
|
|
|
16
21
|
COLLECTION_HELP = (
|
|
17
22
|
"JSON collection: an array path such as '$.data.items[]', or its short "
|
|
@@ -21,29 +26,28 @@ COLLECTION_HELP = (
|
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def collection_paths(values: list[str] | None) -> list[str] | None:
|
|
24
|
-
"""``--collection`` values as absolute paths
|
|
25
|
-
value is the keys leading to the array, so ``data.items`` is
|
|
26
|
-
``$.data.items[]``, and ``.`` is the root array, ``$[]``; a collection is always an array, so ``[]`` is implied."""
|
|
29
|
+
"""``--collection`` values as absolute paths (see ``expand_collection``)."""
|
|
27
30
|
if values is None:
|
|
28
31
|
return None
|
|
29
32
|
paths = []
|
|
30
33
|
for value in values:
|
|
31
|
-
if value.startswith("$"):
|
|
32
|
-
paths.append(value)
|
|
33
|
-
continue
|
|
34
|
-
if value == ".":
|
|
35
|
-
paths.append(format_absolute((ITEMS,)))
|
|
36
|
-
continue
|
|
37
34
|
try:
|
|
38
|
-
|
|
35
|
+
paths.append(expand_collection(value))
|
|
39
36
|
except ValueError as exc:
|
|
40
37
|
raise typer.BadParameter(str(exc), param_hint="--collection") from exc
|
|
41
|
-
if not path or path[-1] != ITEMS:
|
|
42
|
-
path = (*path, ITEMS)
|
|
43
|
-
paths.append(format_absolute(path))
|
|
44
38
|
return paths
|
|
45
39
|
|
|
46
40
|
|
|
41
|
+
def error_text(error: TabalystError, command: str) -> str:
|
|
42
|
+
"""The message of an error, with the commands to copy when a source has
|
|
43
|
+
several equally plausible collections."""
|
|
44
|
+
if isinstance(error, UndecidedCollection) and error.eligible:
|
|
45
|
+
noun = "table" if error.source.suffix.lower() in EXCEL_SUFFIXES else "collection"
|
|
46
|
+
lines = choice_lines(command, error.source, error.eligible, noun=noun)
|
|
47
|
+
return "\n".join([str(error), *lines])
|
|
48
|
+
return str(error)
|
|
49
|
+
|
|
50
|
+
|
|
47
51
|
def error_exit_code(error: TabalystError) -> int:
|
|
48
52
|
if isinstance(error, ConfigurationError):
|
|
49
53
|
return 2
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Choosing a collection from the command line.
|
|
6
|
+
|
|
7
|
+
``--collection`` takes a short form (``Costs``, ``data.items``) or an absolute
|
|
8
|
+
path (``'$["Sales Q1"]'``). This module expands the former, writes the shortest
|
|
9
|
+
value that names a path, and renders the commands to copy when a source has
|
|
10
|
+
several equally plausible collections.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import unicodedata
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from tabalyst.errors import ConfigurationError
|
|
21
|
+
from tabalyst.scanner.paths import ITEMS, Items, Key, format_absolute, parse_path
|
|
22
|
+
|
|
23
|
+
_BARE_KEY = re.compile(r"[A-Za-z0-9_]+")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UndecidedCollection(ConfigurationError):
|
|
27
|
+
"""A source has no collection to analyze, and the command line, the
|
|
28
|
+
configuration and the detection did not decide one.
|
|
29
|
+
|
|
30
|
+
``eligible`` holds the absolute paths the user can choose from when the
|
|
31
|
+
detection found several equally plausible ones, empty otherwise. The
|
|
32
|
+
message stands alone; the command line adds the commands to copy.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, message: str, *, source: Path, eligible: Sequence[str] = ()
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.source = source
|
|
40
|
+
self.eligible = tuple(eligible)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def expand_collection(value: str) -> str:
|
|
44
|
+
"""A ``--collection`` value as an absolute path.
|
|
45
|
+
|
|
46
|
+
Without the leading ``$``, the value is the keys leading to the array, so
|
|
47
|
+
``data.items`` is ``$.data.items[]``, and ``.`` is the root array, ``$[]``;
|
|
48
|
+
a collection is always an array, so ``[]`` is implied. Raises ``ValueError``
|
|
49
|
+
for a value that is not a path.
|
|
50
|
+
"""
|
|
51
|
+
if value.startswith("$"):
|
|
52
|
+
return value
|
|
53
|
+
if value == ".":
|
|
54
|
+
return format_absolute((ITEMS,))
|
|
55
|
+
path = parse_path(value, numeric_keys=True)
|
|
56
|
+
if not path or path[-1] != ITEMS:
|
|
57
|
+
path = (*path, ITEMS)
|
|
58
|
+
return format_absolute(path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _without_items(path: tuple) -> tuple:
|
|
62
|
+
return path[:-1] if path and isinstance(path[-1], Items) else path
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def collection_argument(path: str) -> str:
|
|
66
|
+
"""The value of ``--collection`` that names ``path``, as typed in a shell.
|
|
67
|
+
|
|
68
|
+
The short form when it expands back to the same path (``Costs``,
|
|
69
|
+
``Sales.Orders``, ``data.items``); otherwise the absolute path in single
|
|
70
|
+
quotes (``'$["Sales Q1"]'``).
|
|
71
|
+
"""
|
|
72
|
+
segments = _without_items(parse_path(path))
|
|
73
|
+
if not segments:
|
|
74
|
+
return "."
|
|
75
|
+
names = [segment.name for segment in segments if isinstance(segment, Key)]
|
|
76
|
+
if len(names) == len(segments) and all(_BARE_KEY.fullmatch(n) for n in names):
|
|
77
|
+
short = ".".join(names)
|
|
78
|
+
if _without_items(parse_path(expand_collection(short))) == segments:
|
|
79
|
+
return short
|
|
80
|
+
return f"'{path}'"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def quoted_source(source: str | Path) -> str:
|
|
84
|
+
text = str(source)
|
|
85
|
+
return f'"{text}"' if " " in text else text
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def choice_lines(
|
|
89
|
+
command: str, source: str | Path, paths: Sequence[str], *, noun: str = "collection"
|
|
90
|
+
) -> list[str]:
|
|
91
|
+
"""The lines that offer to rerun ``command`` once per eligible path, then
|
|
92
|
+
once for all of them, ready to copy."""
|
|
93
|
+
name = quoted_source(source)
|
|
94
|
+
lines = [f"Choose the {noun} to analyze:"]
|
|
95
|
+
lines.extend(
|
|
96
|
+
f" tabalyst {command} {name} --collection {collection_argument(path)}"
|
|
97
|
+
for path in paths
|
|
98
|
+
)
|
|
99
|
+
if command == "report":
|
|
100
|
+
lines.append(f"Or report every {noun}: tabalyst report {name} --all-collections")
|
|
101
|
+
return lines
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def collection_slug(path: str, position: int) -> str:
|
|
105
|
+
"""A file-name part for a collection: its keys, lower case, accents removed,
|
|
106
|
+
anything else a hyphen (``$["Sales Q1"]`` is ``sales-q1``). ``position`` is
|
|
107
|
+
the 1-based rank of the collection, for a name with nothing to keep."""
|
|
108
|
+
names = [s.name for s in _without_items(parse_path(path)) if isinstance(s, Key)]
|
|
109
|
+
text = unicodedata.normalize("NFKD", "-".join(names))
|
|
110
|
+
text = "".join(char for char in text if not unicodedata.combining(char))
|
|
111
|
+
slug = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
112
|
+
if slug:
|
|
113
|
+
return slug
|
|
114
|
+
return "root" if not names else f"collection-{position}"
|
|
@@ -20,6 +20,7 @@ from typing import Any, Literal
|
|
|
20
20
|
|
|
21
21
|
from tabalyst.config import merge_settings
|
|
22
22
|
from tabalyst.errors import ConfigurationError
|
|
23
|
+
from tabalyst.inspector.choices import UndecidedCollection
|
|
23
24
|
from tabalyst.inspector.excel_inspect import inspect_workbook
|
|
24
25
|
from tabalyst.inspector.excel_inspect import parameters as excel_parameters
|
|
25
26
|
from tabalyst.inspector.json_inspect import inspect_source
|
|
@@ -183,6 +184,30 @@ def resolve_interpretation(
|
|
|
183
184
|
)
|
|
184
185
|
|
|
185
186
|
|
|
187
|
+
def detect_collections(
|
|
188
|
+
source: str | Path,
|
|
189
|
+
*,
|
|
190
|
+
scan_layer: dict[str, Any] | None = None,
|
|
191
|
+
location: StorageLocation | None = None,
|
|
192
|
+
) -> tuple[InspectDocument, tuple[str, ...]]:
|
|
193
|
+
"""The automatic detection of a JSON or Excel source: every candidate
|
|
194
|
+
collection, whatever the selection, and the notices to print.
|
|
195
|
+
|
|
196
|
+
It is the detection ``resolve_interpretation`` makes, cached the same way,
|
|
197
|
+
and ignores the visible Inspect file and the collection that a
|
|
198
|
+
configuration file names: it lists what the source holds.
|
|
199
|
+
"""
|
|
200
|
+
source = Path(source)
|
|
201
|
+
source_format = source_format_of(source)
|
|
202
|
+
if source_format not in ("json", "excel"):
|
|
203
|
+
raise ValueError(f"{source.name} has no collections to detect.")
|
|
204
|
+
layer = scan_layer or {}
|
|
205
|
+
if source_format == "excel":
|
|
206
|
+
layer = _without_collections(layer)
|
|
207
|
+
document, _, notices, _ = _detect(source, layer, location, None)
|
|
208
|
+
return document, notices
|
|
209
|
+
|
|
210
|
+
|
|
186
211
|
def check_result(interpretation: Interpretation, result: ScanResult) -> tuple[str, ...]:
|
|
187
212
|
"""Compare a scan result with the interpretation it was made with.
|
|
188
213
|
|
|
@@ -423,7 +448,7 @@ def _suspension(
|
|
|
423
448
|
visible: VisibleInspect | None,
|
|
424
449
|
detected: InspectDocument | None,
|
|
425
450
|
notices: Sequence[str] = (),
|
|
426
|
-
) ->
|
|
451
|
+
) -> UndecidedCollection:
|
|
427
452
|
if visible is not None:
|
|
428
453
|
candidates = visible.candidates
|
|
429
454
|
basis = visible.selection_basis
|
|
@@ -469,7 +494,15 @@ def _suspension(
|
|
|
469
494
|
)
|
|
470
495
|
# What was found out on the way, such as a cache that could not be written.
|
|
471
496
|
lines.extend(notices)
|
|
472
|
-
return
|
|
497
|
+
return UndecidedCollection(
|
|
498
|
+
"\n".join(lines), source=source, eligible=_choices(basis, eligible)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _choices(basis: str | None, eligible: Sequence[CandidateSummary]) -> list[str]:
|
|
503
|
+
"""What the user can choose from: only when the detection found several
|
|
504
|
+
equally plausible candidates."""
|
|
505
|
+
return [item.path for item in eligible] if basis == "ambiguous" else []
|
|
473
506
|
|
|
474
507
|
|
|
475
508
|
def _excel_suspension(
|
|
@@ -479,7 +512,7 @@ def _excel_suspension(
|
|
|
479
512
|
eligible: Sequence[CandidateSummary],
|
|
480
513
|
visible: VisibleInspect | None,
|
|
481
514
|
notices: Sequence[str],
|
|
482
|
-
) ->
|
|
515
|
+
) -> UndecidedCollection:
|
|
483
516
|
if basis == "ambiguous":
|
|
484
517
|
why = f"{len(eligible)} tables of {source.name} are equally plausible."
|
|
485
518
|
elif basis == "candidates_truncated":
|
|
@@ -511,7 +544,9 @@ def _excel_suspension(
|
|
|
511
544
|
"config.structure.dataset_path in the file it writes."
|
|
512
545
|
)
|
|
513
546
|
lines.extend(notices)
|
|
514
|
-
return
|
|
547
|
+
return UndecidedCollection(
|
|
548
|
+
"\n".join(lines), source=source, eligible=_choices(basis, eligible)
|
|
549
|
+
)
|
|
515
550
|
|
|
516
551
|
|
|
517
552
|
_REASONS = {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Notices for command-line options that a source cannot use.
|
|
6
|
+
|
|
7
|
+
``--delimiter`` and ``--encoding`` read CSV files, ``--collection`` and
|
|
8
|
+
``--all-collections`` choose among the collections of a JSON file or the tables
|
|
9
|
+
of a workbook. On another kind of source they have nothing to act on: they are
|
|
10
|
+
ignored, and the user is told so instead of believing they applied.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from tabalyst.scanner.identity import source_format_of
|
|
17
|
+
|
|
18
|
+
_ONE_DATASET = {
|
|
19
|
+
"csv": "a CSV file holds one table",
|
|
20
|
+
"jsonl": "a JSONL file holds one dataset, its lines",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ignored_options(
|
|
25
|
+
source: Path,
|
|
26
|
+
*,
|
|
27
|
+
delimiter: str | None = None,
|
|
28
|
+
encoding: str | None = None,
|
|
29
|
+
collections: Sequence[str] | None = None,
|
|
30
|
+
all_collections: bool = False,
|
|
31
|
+
) -> list[str]:
|
|
32
|
+
"""Sentences for the options given that ``source`` does not use."""
|
|
33
|
+
source_format = source_format_of(source)
|
|
34
|
+
notices: list[str] = []
|
|
35
|
+
if source_format != "csv":
|
|
36
|
+
names = [
|
|
37
|
+
name
|
|
38
|
+
for name, value in (("--delimiter", delimiter), ("--encoding", encoding))
|
|
39
|
+
if value is not None
|
|
40
|
+
]
|
|
41
|
+
if names:
|
|
42
|
+
verb = "is" if len(names) == 1 else "are"
|
|
43
|
+
notices.append(
|
|
44
|
+
f"{' and '.join(names)} {verb} ignored: {source.name} is not a CSV file."
|
|
45
|
+
)
|
|
46
|
+
reason = _ONE_DATASET.get(source_format)
|
|
47
|
+
if reason is not None:
|
|
48
|
+
if collections:
|
|
49
|
+
notices.append(f"--collection is ignored: {reason}.")
|
|
50
|
+
if all_collections:
|
|
51
|
+
notices.append(
|
|
52
|
+
f"--all-collections has no effect: {reason}, reported as usual."
|
|
53
|
+
)
|
|
54
|
+
return notices
|