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.
Files changed (167) hide show
  1. {tabalyst-0.6.0/src/tabalyst.egg-info → tabalyst-0.6.1}/PKG-INFO +39 -6
  2. {tabalyst-0.6.0 → tabalyst-0.6.1}/README.md +38 -5
  3. {tabalyst-0.6.0 → tabalyst-0.6.1}/pyproject.toml +1 -1
  4. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/batch.py +11 -3
  5. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/inspect.py +6 -9
  6. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/report.py +26 -5
  7. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/scan.py +8 -2
  8. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/terminal.py +18 -14
  9. tabalyst-0.6.1/src/tabalyst/inspector/choices.py +114 -0
  10. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/resolution.py +39 -4
  11. tabalyst-0.6.1/src/tabalyst/option_notices.py +54 -0
  12. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/report_service.py +185 -4
  13. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scan_service.py +17 -1
  14. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/static/theme.css +1 -0
  15. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/templates/column.html +2 -2
  16. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/templates/report.html +2 -2
  17. {tabalyst-0.6.0 → tabalyst-0.6.1/src/tabalyst.egg-info}/PKG-INFO +39 -6
  18. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/SOURCES.txt +4 -0
  19. tabalyst-0.6.1/tests/test_all_collections.py +312 -0
  20. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_cli.py +2 -2
  21. tabalyst-0.6.1/tests/test_option_notices.py +89 -0
  22. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_reporting.py +4 -3
  23. {tabalyst-0.6.0 → tabalyst-0.6.1}/LICENSE +0 -0
  24. {tabalyst-0.6.0 → tabalyst-0.6.1}/MANIFEST.in +0 -0
  25. {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/README.md +0 -0
  26. {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/config.json +0 -0
  27. {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/input/basic.csv +0 -0
  28. {tabalyst-0.6.0 → tabalyst-0.6.1}/examples/input/insurance-customers.csv +0 -0
  29. {tabalyst-0.6.0 → tabalyst-0.6.1}/setup.cfg +0 -0
  30. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/__init__.py +0 -0
  31. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/__main__.py +0 -0
  32. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/_version.py +0 -0
  33. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cache_service.py +0 -0
  34. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/__init__.py +0 -0
  35. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/app.py +0 -0
  36. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/cache.py +0 -0
  37. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/cli/sample.py +0 -0
  38. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/config.py +0 -0
  39. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/errors.py +0 -0
  40. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/execution_log.py +0 -0
  41. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspect_service.py +0 -0
  42. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/__init__.py +0 -0
  43. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/__init__.py +0 -0
  44. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/build.py +0 -0
  45. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/detect.py +0 -0
  46. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/excel_inspect/parameters.py +0 -0
  47. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/__init__.py +0 -0
  48. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/build.py +0 -0
  49. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/detect.py +0 -0
  50. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/json_inspect/parameters.py +0 -0
  51. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/models.py +0 -0
  52. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/inspector/persistence.py +0 -0
  53. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/models.py +0 -0
  54. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/progress.py +0 -0
  55. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/project_open_service.py +0 -0
  56. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/project_scan_service.py +0 -0
  57. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/__init__.py +0 -0
  58. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_bounded_values.py +0 -0
  59. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_cache.py +0 -0
  60. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_codec.py +0 -0
  61. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_generation.py +0 -0
  62. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_locks.py +0 -0
  63. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_publication.py +0 -0
  64. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_queries.py +0 -0
  65. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_query_budget.py +0 -0
  66. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_recovery.py +0 -0
  67. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_retention.py +0 -0
  68. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_schema.py +0 -0
  69. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_session.py +0 -0
  70. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_staging.py +0 -0
  71. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_sync.py +0 -0
  72. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/_validation.py +0 -0
  73. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/catalog.py +0 -0
  74. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/freshness.py +0 -0
  75. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/identity.py +0 -0
  76. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/index.py +0 -0
  77. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/location.py +0 -0
  78. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/models.py +0 -0
  79. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/projects/store.py +0 -0
  80. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/report_config.py +0 -0
  81. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/report_profile.py +0 -0
  82. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/reporting.py +0 -0
  83. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/sampling.py +0 -0
  84. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/sampling_service.py +0 -0
  85. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scan_reuse.py +0 -0
  86. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/__init__.py +0 -0
  87. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/api.py +0 -0
  88. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/config.py +0 -0
  89. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/__init__.py +0 -0
  90. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/amount.py +0 -0
  91. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/base.py +0 -0
  92. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/boolean.py +0 -0
  93. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/currency.py +0 -0
  94. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/email.py +0 -0
  95. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
  96. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/ip.py +0 -0
  97. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/names.py +0 -0
  98. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/number.py +0 -0
  99. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/pattern.py +0 -0
  100. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/percentage.py +0 -0
  101. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/phone.py +0 -0
  102. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/postal.py +0 -0
  103. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/quantity.py +0 -0
  104. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/registry.py +0 -0
  105. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/shape.py +0 -0
  106. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/temporal.py +0 -0
  107. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/url.py +0 -0
  108. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/detectors/uuid.py +0 -0
  109. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/diagnostics.py +0 -0
  110. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/engine.py +0 -0
  111. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/exposure.py +0 -0
  112. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/field.py +0 -0
  113. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/identity.py +0 -0
  114. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/measures.py +0 -0
  115. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/models.py +0 -0
  116. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/normalization.py +0 -0
  117. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/observations.py +0 -0
  118. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/paths.py +0 -0
  119. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/__init__.py +0 -0
  120. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/base.py +0 -0
  121. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
  122. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/excel_common.py +0 -0
  123. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/excel_reader.py +0 -0
  124. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/excel_table.py +0 -0
  125. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/json_common.py +0 -0
  126. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/json_reader.py +0 -0
  127. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/readers/jsonl_reader.py +0 -0
  128. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/records.py +0 -0
  129. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/structure.py +0 -0
  130. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/technical.py +0 -0
  131. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/values.py +0 -0
  132. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/scanner/workers.py +0 -0
  133. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/service.py +0 -0
  134. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/shared_scan_service.py +0 -0
  135. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst/static/report.js +0 -0
  136. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/dependency_links.txt +0 -0
  137. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/entry_points.txt +0 -0
  138. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/requires.txt +0 -0
  139. {tabalyst-0.6.0 → tabalyst-0.6.1}/src/tabalyst.egg-info/top_level.txt +0 -0
  140. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_license_headers.py +0 -0
  141. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_cache.py +0 -0
  142. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_database.py +0 -0
  143. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_generations.py +0 -0
  144. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_identity.py +0 -0
  145. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_index.py +0 -0
  146. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_location.py +0 -0
  147. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_open_service.py +0 -0
  148. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_queries.py +0 -0
  149. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_rescan.py +0 -0
  150. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_scan.py +0 -0
  151. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_sessions.py +0 -0
  152. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_project_store.py +0 -0
  153. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_public_api.py +0 -0
  154. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_sampling.py +0 -0
  155. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scan_command.py +0 -0
  156. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_amounts.py +0 -0
  157. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_batches.py +0 -0
  158. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_config.py +0 -0
  159. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_csv.py +0 -0
  160. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_email_url.py +0 -0
  161. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_exposure.py +0 -0
  162. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_paths.py +0 -0
  163. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_phone.py +0 -0
  164. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_postal.py +0 -0
  165. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_scanner_uuid_ip.py +0 -0
  166. {tabalyst-0.6.0 → tabalyst-0.6.1}/tests/test_shared_scan_cli.py +0 -0
  167. {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.0
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 lists them instead of choosing: pass `--collection Costs` (or
147
- `'$.Sales.Orders'` for a named table), or see [Tabalyst Inspect](#tabalyst-inspect).
148
- Older `.xls` files are not read: save them as `.xlsx`.
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(["data/*.json", "logs/*.jsonl", "*.xlsx"])
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 — Catalyseur Numérique.
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 lists them instead of choosing: pass `--collection Costs` (or
109
- `'$.Sales.Orders'` for a named table), or see [Tabalyst Inspect](#tabalyst-inspect).
110
- Older `.xls` files are not read: save them as `.xlsx`.
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(["data/*.json", "logs/*.jsonl", "*.xlsx"])
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 — Catalyseur Numérique.
505
+ Created by Gregory Borelli. Open source project by Loribel Labs.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabalyst"
7
- version = "0.6.0"
7
+ version = "0.6.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"
@@ -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 = Path(output_dir) if output_dir is not None else None
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
- source = str(success.source)
54
- if " " in source:
55
- source = f'"{source}"'
56
- lines = ["Choose the collection to analyze:"]
57
- lines.extend(
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(f"Error [{failure.job.source}]: {failure.error}", err=True)
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 or JSON files, or scan documents with --scan, "
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="Directory for reports named after their source files.",
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(f"Error [{failure.job.source}]: {failure.error}", err=True)
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.paths import ITEMS, format_absolute, parse_path
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. Without the leading ``$``, the
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
- path = parse_path(value, numeric_keys=True)
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
- ) -> ConfigurationError:
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 ConfigurationError("\n".join(lines))
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
- ) -> ConfigurationError:
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 ConfigurationError("\n".join(lines))
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