tabalyst 0.4.2__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tabalyst-0.4.2/src/tabalyst.egg-info → tabalyst-0.4.3}/PKG-INFO +35 -9
- {tabalyst-0.4.2 → tabalyst-0.4.3}/README.md +31 -7
- {tabalyst-0.4.2 → tabalyst-0.4.3}/pyproject.toml +4 -2
- tabalyst-0.4.3/src/tabalyst/cache_service.py +114 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/cli/app.py +2 -0
- tabalyst-0.4.3/src/tabalyst/cli/cache.py +64 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/cli/report.py +2 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/cli/scan.py +3 -0
- tabalyst-0.4.3/src/tabalyst/project_open_service.py +179 -0
- tabalyst-0.4.3/src/tabalyst/project_scan_service.py +204 -0
- tabalyst-0.4.3/src/tabalyst/projects/__init__.py +48 -0
- tabalyst-0.4.3/src/tabalyst/projects/_bounded_values.py +449 -0
- tabalyst-0.4.3/src/tabalyst/projects/_cache.py +370 -0
- tabalyst-0.4.3/src/tabalyst/projects/_codec.py +116 -0
- tabalyst-0.4.3/src/tabalyst/projects/_generation.py +118 -0
- tabalyst-0.4.3/src/tabalyst/projects/_locks.py +144 -0
- tabalyst-0.4.3/src/tabalyst/projects/_publication.py +278 -0
- tabalyst-0.4.3/src/tabalyst/projects/_queries.py +481 -0
- tabalyst-0.4.3/src/tabalyst/projects/_query_budget.py +83 -0
- tabalyst-0.4.3/src/tabalyst/projects/_recovery.py +59 -0
- tabalyst-0.4.3/src/tabalyst/projects/_retention.py +70 -0
- tabalyst-0.4.3/src/tabalyst/projects/_schema.py +115 -0
- tabalyst-0.4.3/src/tabalyst/projects/_session.py +226 -0
- tabalyst-0.4.3/src/tabalyst/projects/_staging.py +439 -0
- tabalyst-0.4.3/src/tabalyst/projects/_sync.py +42 -0
- tabalyst-0.4.3/src/tabalyst/projects/_validation.py +459 -0
- tabalyst-0.4.3/src/tabalyst/projects/catalog.py +75 -0
- tabalyst-0.4.3/src/tabalyst/projects/freshness.py +54 -0
- tabalyst-0.4.3/src/tabalyst/projects/identity.py +42 -0
- tabalyst-0.4.3/src/tabalyst/projects/index.py +175 -0
- tabalyst-0.4.3/src/tabalyst/projects/location.py +118 -0
- tabalyst-0.4.3/src/tabalyst/projects/models.py +110 -0
- tabalyst-0.4.3/src/tabalyst/projects/store.py +117 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/report_service.py +13 -2
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scan_reuse.py +51 -16
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scan_service.py +65 -25
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/records.py +20 -13
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/service.py +34 -4
- tabalyst-0.4.3/src/tabalyst/shared_scan_service.py +149 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/static/report.js +35 -24
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/static/theme.css +6 -1
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/templates/report.html +12 -6
- {tabalyst-0.4.2 → tabalyst-0.4.3/src/tabalyst.egg-info}/PKG-INFO +35 -9
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst.egg-info/SOURCES.txt +43 -1
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst.egg-info/requires.txt +2 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_cli.py +7 -2
- tabalyst-0.4.3/tests/test_project_cache.py +439 -0
- tabalyst-0.4.3/tests/test_project_database.py +525 -0
- tabalyst-0.4.3/tests/test_project_generations.py +481 -0
- tabalyst-0.4.3/tests/test_project_identity.py +55 -0
- tabalyst-0.4.3/tests/test_project_index.py +172 -0
- tabalyst-0.4.3/tests/test_project_location.py +89 -0
- tabalyst-0.4.3/tests/test_project_open_service.py +300 -0
- tabalyst-0.4.3/tests/test_project_queries.py +718 -0
- tabalyst-0.4.3/tests/test_project_rescan.py +536 -0
- tabalyst-0.4.3/tests/test_project_scan.py +238 -0
- tabalyst-0.4.3/tests/test_project_sessions.py +441 -0
- tabalyst-0.4.3/tests/test_project_store.py +238 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_public_api.py +12 -15
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scan_command.py +43 -17
- tabalyst-0.4.3/tests/test_shared_scan_cli.py +130 -0
- tabalyst-0.4.3/tests/test_source_freshness.py +79 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/LICENSE +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/MANIFEST.in +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/examples/README.md +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/examples/config.json +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/examples/input/basic.csv +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/setup.cfg +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/__init__.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/batch.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/cli/sample.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/cli/terminal.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/config.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/execution_log.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/models.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/progress.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/report_config.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/report_profile.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/reporting.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/sampling.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/sampling_service.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/__init__.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/api.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/config.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/__init__.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/amount.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/base.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/boolean.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/currency.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/email.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/ip.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/names.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/number.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/pattern.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/percentage.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/phone.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/postal.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/quantity.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/registry.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/shape.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/temporal.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/url.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/detectors/uuid.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/diagnostics.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/engine.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/exposure.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/field.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/measures.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/models.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/normalization.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/observations.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/paths.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/__init__.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/base.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/readers/json_reader.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/structure.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/technical.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/values.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst/scanner/workers.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/src/tabalyst.egg-info/top_level.txt +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_reporting.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_sampling.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_amounts.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_batches.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_config.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_csv.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_email_url.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_exposure.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_paths.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_phone.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_postal.py +0 -0
- {tabalyst-0.4.2 → tabalyst-0.4.3}/tests/test_scanner_uuid_ip.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tabalyst
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Open-source toolkit for understanding and working with unfamiliar data.
|
|
5
5
|
Author: Gregory Borelli
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/loribel-labs/tabalyst
|
|
|
8
8
|
Project-URL: Repository, https://github.com/loribel-labs/tabalyst.git
|
|
9
9
|
Project-URL: Issues, https://github.com/loribel-labs/tabalyst/issues
|
|
10
10
|
Keywords: csv,data-analysis,data-profiling,data-quality,eda,html-report,json
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
@@ -21,8 +21,10 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
+
Requires-Dist: duckdb==1.5.5
|
|
24
25
|
Requires-Dist: ijson>=3.3
|
|
25
26
|
Requires-Dist: jinja2>=3.1
|
|
27
|
+
Requires-Dist: platformdirs>=4
|
|
26
28
|
Requires-Dist: pydantic>=2.11
|
|
27
29
|
Requires-Dist: typer>=0.12
|
|
28
30
|
Provides-Extra: dev
|
|
@@ -42,8 +44,10 @@ with structured data.
|
|
|
42
44
|
Its first tool is **Tabalyst Report**. The current CSV implementation,
|
|
43
45
|
**Tabalyst CSV Report**, analyzes a CSV file and produces both a structured JSON
|
|
44
46
|
profile and a self-contained interactive HTML report.
|
|
47
|
+
**Tabalyst Scan** describes CSV and JSON files in a scan document, and
|
|
48
|
+
**Tabalyst Sample** creates smaller CSV files.
|
|
45
49
|
|
|
46
|
-
Tabalyst is in
|
|
50
|
+
Tabalyst is in beta. Its interfaces may still change while
|
|
47
51
|
the shared toolkit architecture is being established.
|
|
48
52
|
|
|
49
53
|
## Install
|
|
@@ -54,7 +58,7 @@ Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
|
|
|
54
58
|
pip install --upgrade tabalyst
|
|
55
59
|
```
|
|
56
60
|
|
|
57
|
-
The same command installs Tabalyst or updates it. Tabalyst is in
|
|
61
|
+
The same command installs Tabalyst or updates it. Tabalyst is in beta and
|
|
58
62
|
changes often: update it before each new test.
|
|
59
63
|
|
|
60
64
|
## Tabalyst Report
|
|
@@ -225,8 +229,10 @@ tabalyst scan customers.csv
|
|
|
225
229
|
tabalyst scan orders.json --collection "$.customers[]"
|
|
226
230
|
```
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
For a CSV, the default scan is the project's generation-bound `scan.json` under
|
|
233
|
+
Tabalyst's local storage directory (or `TABALYST_HOME`). The project also holds
|
|
234
|
+
the database and disposable `cache/` used by future tools. `-o` or `-d` writes
|
|
235
|
+
a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
|
|
230
236
|
a stream, with memory bounded by configurable limits, and records for each
|
|
231
237
|
field its presence, native types, missing values, frequencies, exact
|
|
232
238
|
statistics, normalization variants, technical type and the result of every
|
|
@@ -238,7 +244,14 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
|
|
|
238
244
|
analyzed by several worker processes, with the same result; `--workers`
|
|
239
245
|
chooses their number, `--workers 1` keeps one process.
|
|
240
246
|
|
|
241
|
-
|
|
247
|
+
`tabalyst report customers.csv` reuses the verified project scan when the CSV
|
|
248
|
+
content and requested scan settings are current. It creates a project scan when
|
|
249
|
+
none exists. If the CSV changed, it publishes a new generation, removes the old
|
|
250
|
+
generation and its owned cache when no reader holds it, then builds the report.
|
|
251
|
+
The source check uses size, modification time and SHA-256. An old generation
|
|
252
|
+
held by a reader is retained with a warning.
|
|
253
|
+
|
|
254
|
+
Build the report from a standalone scan document without reading the source again:
|
|
242
255
|
|
|
243
256
|
```console
|
|
244
257
|
tabalyst report --scan customers.scan.json
|
|
@@ -247,6 +260,18 @@ tabalyst report --scan customers.scan.json
|
|
|
247
260
|
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
248
261
|
settings differ from the `scan` settings of `--config`.
|
|
249
262
|
|
|
263
|
+
Inspect or clean disposable project query caches for every project, or for one
|
|
264
|
+
CSV file:
|
|
265
|
+
|
|
266
|
+
```console
|
|
267
|
+
tabalyst cache info
|
|
268
|
+
tabalyst cache info customers.csv
|
|
269
|
+
tabalyst cache clean
|
|
270
|
+
tabalyst cache clean customers.csv
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`cache clean` leaves project scans and databases in place.
|
|
274
|
+
|
|
250
275
|
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
251
276
|
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
252
277
|
|
|
@@ -301,8 +326,9 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
|
301
326
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
302
327
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
303
328
|
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
304
|
-
writes one `.scan.json` document per source
|
|
305
|
-
|
|
329
|
+
writes one standalone `.scan.json` document per source by default. Pass
|
|
330
|
+
`project_storage=True` to use project storage for CSV files. `from_scan=True`
|
|
331
|
+
builds reports from standalone scan documents.
|
|
306
332
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
307
333
|
|
|
308
334
|
Existing artifacts are never replaced silently. Pass `force=True` when
|
|
@@ -8,8 +8,10 @@ with structured data.
|
|
|
8
8
|
Its first tool is **Tabalyst Report**. The current CSV implementation,
|
|
9
9
|
**Tabalyst CSV Report**, analyzes a CSV file and produces both a structured JSON
|
|
10
10
|
profile and a self-contained interactive HTML report.
|
|
11
|
+
**Tabalyst Scan** describes CSV and JSON files in a scan document, and
|
|
12
|
+
**Tabalyst Sample** creates smaller CSV files.
|
|
11
13
|
|
|
12
|
-
Tabalyst is in
|
|
14
|
+
Tabalyst is in beta. Its interfaces may still change while
|
|
13
15
|
the shared toolkit architecture is being established.
|
|
14
16
|
|
|
15
17
|
## Install
|
|
@@ -20,7 +22,7 @@ Tabalyst supports Python 3.11, 3.12, 3.13, and 3.14.
|
|
|
20
22
|
pip install --upgrade tabalyst
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
The same command installs Tabalyst or updates it. Tabalyst is in
|
|
25
|
+
The same command installs Tabalyst or updates it. Tabalyst is in beta and
|
|
24
26
|
changes often: update it before each new test.
|
|
25
27
|
|
|
26
28
|
## Tabalyst Report
|
|
@@ -191,8 +193,10 @@ tabalyst scan customers.csv
|
|
|
191
193
|
tabalyst scan orders.json --collection "$.customers[]"
|
|
192
194
|
```
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
For a CSV, the default scan is the project's generation-bound `scan.json` under
|
|
197
|
+
Tabalyst's local storage directory (or `TABALYST_HOME`). The project also holds
|
|
198
|
+
the database and disposable `cache/` used by future tools. `-o` or `-d` writes
|
|
199
|
+
a standalone `<stem>.scan.json` export instead. Tabalyst Scan reads the file once as
|
|
196
200
|
a stream, with memory bounded by configurable limits, and records for each
|
|
197
201
|
field its presence, native types, missing values, frequencies, exact
|
|
198
202
|
statistics, normalization variants, technical type and the result of every
|
|
@@ -204,7 +208,14 @@ interrupted scan never leaves a partial file. Files of 16 MiB or more are
|
|
|
204
208
|
analyzed by several worker processes, with the same result; `--workers`
|
|
205
209
|
chooses their number, `--workers 1` keeps one process.
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
`tabalyst report customers.csv` reuses the verified project scan when the CSV
|
|
212
|
+
content and requested scan settings are current. It creates a project scan when
|
|
213
|
+
none exists. If the CSV changed, it publishes a new generation, removes the old
|
|
214
|
+
generation and its owned cache when no reader holds it, then builds the report.
|
|
215
|
+
The source check uses size, modification time and SHA-256. An old generation
|
|
216
|
+
held by a reader is retained with a warning.
|
|
217
|
+
|
|
218
|
+
Build the report from a standalone scan document without reading the source again:
|
|
208
219
|
|
|
209
220
|
```console
|
|
210
221
|
tabalyst report --scan customers.scan.json
|
|
@@ -213,6 +224,18 @@ tabalyst report --scan customers.scan.json
|
|
|
213
224
|
Tabalyst refuses a scan whose source changed since it was written, or whose
|
|
214
225
|
settings differ from the `scan` settings of `--config`.
|
|
215
226
|
|
|
227
|
+
Inspect or clean disposable project query caches for every project, or for one
|
|
228
|
+
CSV file:
|
|
229
|
+
|
|
230
|
+
```console
|
|
231
|
+
tabalyst cache info
|
|
232
|
+
tabalyst cache info customers.csv
|
|
233
|
+
tabalyst cache clean
|
|
234
|
+
tabalyst cache clean customers.csv
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`cache clean` leaves project scans and databases in place.
|
|
238
|
+
|
|
216
239
|
See [Scan CSV and JSON files](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/how-to/scan-files.md)
|
|
217
240
|
and the [scan format](https://github.com/loribel-labs/tabalyst/blob/main/docs/en/reference/scan-format.md).
|
|
218
241
|
|
|
@@ -267,8 +290,9 @@ reports = tabalyst.generate_reports(["scans/*.scan.json"], from_scan=True)
|
|
|
267
290
|
`analyze()` returns the JSON-serializable profile for one report.
|
|
268
291
|
`generate_reports()` returns the complete batch plan, successes, and failures.
|
|
269
292
|
`scan()` returns a scan result without writing anything; `generate_scans()`
|
|
270
|
-
writes one `.scan.json` document per source
|
|
271
|
-
|
|
293
|
+
writes one standalone `.scan.json` document per source by default. Pass
|
|
294
|
+
`project_storage=True` to use project storage for CSV files. `from_scan=True`
|
|
295
|
+
builds reports from standalone scan documents.
|
|
272
296
|
Expected failures derive from `tabalyst.TabalystError`.
|
|
273
297
|
|
|
274
298
|
Existing artifacts are never replaced silently. Pass `force=True` when
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tabalyst"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "Open-source toolkit for understanding and working with unfamiliar data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -22,7 +22,7 @@ keywords = [
|
|
|
22
22
|
"json",
|
|
23
23
|
]
|
|
24
24
|
classifiers = [
|
|
25
|
-
"Development Status ::
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
26
|
"Intended Audience :: Developers",
|
|
27
27
|
"Intended Audience :: Science/Research",
|
|
28
28
|
"Operating System :: OS Independent",
|
|
@@ -34,8 +34,10 @@ classifiers = [
|
|
|
34
34
|
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
|
+
"duckdb==1.5.5",
|
|
37
38
|
"ijson>=3.3",
|
|
38
39
|
"jinja2>=3.1",
|
|
40
|
+
"platformdirs>=4",
|
|
39
41
|
"pydantic>=2.11",
|
|
40
42
|
"typer>=0.12",
|
|
41
43
|
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Public cleanup of disposable project query caches."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tabalyst.errors import InputError
|
|
7
|
+
from tabalyst.projects._cache import clean_query_caches, inventory_query_caches
|
|
8
|
+
from tabalyst.projects._locks import ProjectBusyError
|
|
9
|
+
from tabalyst.projects.identity import is_project_id
|
|
10
|
+
from tabalyst.projects.index import (
|
|
11
|
+
document_key,
|
|
12
|
+
find_project,
|
|
13
|
+
read_index,
|
|
14
|
+
source_key,
|
|
15
|
+
)
|
|
16
|
+
from tabalyst.projects.location import StorageLocation
|
|
17
|
+
from tabalyst.projects.store import read_project
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CacheCleanResult:
|
|
22
|
+
projects: int
|
|
23
|
+
removed: int
|
|
24
|
+
removed_bytes: int
|
|
25
|
+
skipped: tuple[str, ...]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CacheInfo:
|
|
30
|
+
projects: int
|
|
31
|
+
directories: int
|
|
32
|
+
logical_bytes: int
|
|
33
|
+
skipped: tuple[str, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _selected_projects(
|
|
37
|
+
source: str | Path | None = None,
|
|
38
|
+
*,
|
|
39
|
+
location: StorageLocation | None = None,
|
|
40
|
+
) -> tuple[StorageLocation, list]:
|
|
41
|
+
location = location or StorageLocation.local()
|
|
42
|
+
if source is None:
|
|
43
|
+
try:
|
|
44
|
+
directories = sorted(location.projects_dir.iterdir())
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
directories = []
|
|
47
|
+
projects = []
|
|
48
|
+
for directory in directories:
|
|
49
|
+
if is_project_id(directory.name):
|
|
50
|
+
try:
|
|
51
|
+
projects.append(read_project(location, directory.name))
|
|
52
|
+
except InputError:
|
|
53
|
+
continue
|
|
54
|
+
else:
|
|
55
|
+
key = source_key(Path(source))
|
|
56
|
+
index = read_index(location)
|
|
57
|
+
project_id = index.projects.get(key) if index else None
|
|
58
|
+
try:
|
|
59
|
+
selected = read_project(location, project_id) if project_id else None
|
|
60
|
+
except InputError:
|
|
61
|
+
selected = None
|
|
62
|
+
if selected is None or document_key(selected) != key:
|
|
63
|
+
selected = find_project(location, Path(source))
|
|
64
|
+
if selected is None:
|
|
65
|
+
raise InputError(f"No Tabalyst project for source: {source}")
|
|
66
|
+
projects = [selected]
|
|
67
|
+
return location, projects
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cache_info(
|
|
71
|
+
source: str | Path | None = None,
|
|
72
|
+
*,
|
|
73
|
+
location: StorageLocation | None = None,
|
|
74
|
+
) -> CacheInfo:
|
|
75
|
+
"""Summarize owned disposable cache files without modifying them."""
|
|
76
|
+
location, projects = _selected_projects(source, location=location)
|
|
77
|
+
directories = 0
|
|
78
|
+
logical_bytes = 0
|
|
79
|
+
skipped = []
|
|
80
|
+
for project in projects:
|
|
81
|
+
inventory = inventory_query_caches(location, project.project_id)
|
|
82
|
+
for entry in inventory.entries:
|
|
83
|
+
if entry.status == "candidate":
|
|
84
|
+
directories += 1
|
|
85
|
+
logical_bytes += entry.logical_bytes
|
|
86
|
+
else:
|
|
87
|
+
skipped.append(f"{project.project_id}/{entry.path}: {entry.reason}")
|
|
88
|
+
return CacheInfo(len(projects), directories, logical_bytes, tuple(skipped))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def clean_cache(
|
|
92
|
+
source: str | Path | None = None,
|
|
93
|
+
*,
|
|
94
|
+
location: StorageLocation | None = None,
|
|
95
|
+
) -> CacheCleanResult:
|
|
96
|
+
"""Clean owned disposable cache files for one source or all projects."""
|
|
97
|
+
location, projects = _selected_projects(source, location=location)
|
|
98
|
+
removed = 0
|
|
99
|
+
removed_bytes = 0
|
|
100
|
+
skipped = []
|
|
101
|
+
for project in projects:
|
|
102
|
+
plan = inventory_query_caches(location, project.project_id)
|
|
103
|
+
try:
|
|
104
|
+
result = clean_query_caches(location, project.project_id, plan=plan)
|
|
105
|
+
except ProjectBusyError as exc:
|
|
106
|
+
skipped.append(f"{project.project_id}/cache: {exc}")
|
|
107
|
+
continue
|
|
108
|
+
for entry in result.entries:
|
|
109
|
+
if entry.status == "removed":
|
|
110
|
+
removed += 1
|
|
111
|
+
removed_bytes += entry.removed_bytes
|
|
112
|
+
elif entry.status in ("skipped", "error"):
|
|
113
|
+
skipped.append(f"{project.project_id}/{entry.path}: {entry.reason}")
|
|
114
|
+
return CacheCleanResult(len(projects), removed, removed_bytes, tuple(skipped))
|
|
@@ -5,6 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
7
|
from tabalyst import __version__
|
|
8
|
+
from tabalyst.cli.cache import cache_app
|
|
8
9
|
from tabalyst.cli.report import report_command
|
|
9
10
|
from tabalyst.cli.sample import sample_command
|
|
10
11
|
from tabalyst.cli.scan import scan_command
|
|
@@ -41,6 +42,7 @@ def root(
|
|
|
41
42
|
app.command("report")(report_command)
|
|
42
43
|
app.command("sample")(sample_command)
|
|
43
44
|
app.command("scan")(scan_command)
|
|
45
|
+
app.add_typer(cache_app, name="cache")
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
def main() -> None:
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""CLI adapter for disposable project cache cleanup."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from tabalyst.cache_service import cache_info, clean_cache
|
|
9
|
+
from tabalyst.cli.terminal import error_exit_code
|
|
10
|
+
from tabalyst.errors import TabalystError
|
|
11
|
+
|
|
12
|
+
cache_app = typer.Typer(help="Manage disposable project caches.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _count(value: int, singular: str) -> str:
|
|
16
|
+
if value == 1:
|
|
17
|
+
return f"{value} {singular}"
|
|
18
|
+
plural = singular.removesuffix("y") + "ies" if singular.endswith("y") else singular + "s"
|
|
19
|
+
return f"{value} {plural}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cache_app.command("info")
|
|
23
|
+
def info_command(
|
|
24
|
+
source: Annotated[
|
|
25
|
+
Path | None,
|
|
26
|
+
typer.Argument(help="CSV file whose project cache should be inspected."),
|
|
27
|
+
] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Show the size and directory count of all caches or one source cache."""
|
|
30
|
+
try:
|
|
31
|
+
result = cache_info(source)
|
|
32
|
+
except TabalystError as exc:
|
|
33
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
34
|
+
raise typer.Exit(error_exit_code(exc)) from exc
|
|
35
|
+
typer.echo(
|
|
36
|
+
f"Cache: {_count(result.directories, 'directory')} in "
|
|
37
|
+
f"{_count(result.projects, 'project')} "
|
|
38
|
+
f"({result.logical_bytes:,} bytes)."
|
|
39
|
+
)
|
|
40
|
+
for warning in result.skipped:
|
|
41
|
+
typer.echo(f"Retained: {warning}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cache_app.command("clean")
|
|
45
|
+
def clean_command(
|
|
46
|
+
source: Annotated[
|
|
47
|
+
Path | None,
|
|
48
|
+
typer.Argument(help="CSV file whose project cache should be cleaned."),
|
|
49
|
+
] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Clean all project caches, or the cache for one source file."""
|
|
52
|
+
try:
|
|
53
|
+
result = clean_cache(source)
|
|
54
|
+
except TabalystError as exc:
|
|
55
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
56
|
+
raise typer.Exit(error_exit_code(exc)) from exc
|
|
57
|
+
typer.echo(
|
|
58
|
+
f"Cleaned {_count(result.removed, 'cache directory')} in "
|
|
59
|
+
f"{_count(result.projects, 'project')} "
|
|
60
|
+
f"({result.removed_bytes:,} bytes).",
|
|
61
|
+
err=True,
|
|
62
|
+
)
|
|
63
|
+
for warning in result.skipped:
|
|
64
|
+
typer.echo(f"Warning: retained {warning}", err=True)
|
|
@@ -58,6 +58,8 @@ def _print_batch_result(
|
|
|
58
58
|
typer.echo(" " + " | ".join(details), err=True)
|
|
59
59
|
|
|
60
60
|
for success in batch.successes:
|
|
61
|
+
for warning in success.warnings:
|
|
62
|
+
typer.echo(f"Warning [{success.job.source}]: {warning}", err=True)
|
|
61
63
|
if success.source_checked is False:
|
|
62
64
|
typer.echo(
|
|
63
65
|
f"Warning [{success.job.source}]: source "
|
|
@@ -76,6 +76,8 @@ def _print_batch_result(
|
|
|
76
76
|
for success in batch.successes:
|
|
77
77
|
if success.result.status == "partial":
|
|
78
78
|
_print_partial(success)
|
|
79
|
+
for warning in success.warnings:
|
|
80
|
+
typer.echo(f"Warning [{success.job.source}]: {warning}", err=True)
|
|
79
81
|
for failure in batch.failures:
|
|
80
82
|
typer.echo(f"Error [{failure.job.source}]: {failure.error}", err=True)
|
|
81
83
|
|
|
@@ -180,6 +182,7 @@ def scan_command(
|
|
|
180
182
|
force=force,
|
|
181
183
|
on_progress=progress,
|
|
182
184
|
workers=workers,
|
|
185
|
+
project_storage=True,
|
|
183
186
|
)
|
|
184
187
|
except TabalystError as exc:
|
|
185
188
|
typer.echo(f"Error: {exc}", err=True)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Private first-open policy for an eventual Explore adapter (S18).
|
|
2
|
+
|
|
3
|
+
The decision is transient and advisory. A caller must retain it for an explicit
|
|
4
|
+
snapshot or rescan choice; every action revalidates the selected generation.
|
|
5
|
+
No CLI, public API, automatic scan or presentation is provided here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from contextlib import ExitStack, contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from tabalyst.errors import InputError
|
|
14
|
+
from tabalyst.project_scan_service import rescan_project
|
|
15
|
+
from tabalyst.projects._generation import GenerationConflictError
|
|
16
|
+
from tabalyst.projects._session import (
|
|
17
|
+
ProjectSession,
|
|
18
|
+
SessionAssessment,
|
|
19
|
+
SessionRefusedError,
|
|
20
|
+
inspect_project,
|
|
21
|
+
open_project,
|
|
22
|
+
)
|
|
23
|
+
from tabalyst.projects.location import StorageLocation
|
|
24
|
+
from tabalyst.scan_reuse import SourceState
|
|
25
|
+
from tabalyst.scanner import ScanConfig
|
|
26
|
+
from tabalyst.scanner.config import config_sha256
|
|
27
|
+
|
|
28
|
+
OpeningAction = Literal["open_current", "choose", "resolve_config"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ProjectOpenDecision:
|
|
33
|
+
storage_root: Path
|
|
34
|
+
assessment: SessionAssessment
|
|
35
|
+
action: OpeningAction
|
|
36
|
+
choices: tuple[str, ...]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ProjectEntry:
|
|
41
|
+
decision: ProjectOpenDecision
|
|
42
|
+
session: ProjectSession | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _decision(
|
|
46
|
+
location: StorageLocation, assessment: SessionAssessment
|
|
47
|
+
) -> ProjectOpenDecision:
|
|
48
|
+
root = location.root.resolve()
|
|
49
|
+
if assessment.errors:
|
|
50
|
+
choices = ("use_recorded_config",)
|
|
51
|
+
if (
|
|
52
|
+
assessment.source_check is not None
|
|
53
|
+
and assessment.source_check.state is not SourceState.MISSING
|
|
54
|
+
):
|
|
55
|
+
choices += ("rescan",)
|
|
56
|
+
return ProjectOpenDecision(root, assessment, "resolve_config", choices)
|
|
57
|
+
if assessment.current_ready:
|
|
58
|
+
return ProjectOpenDecision(
|
|
59
|
+
root, assessment, "open_current", ("open_current", "rescan")
|
|
60
|
+
)
|
|
61
|
+
if assessment.source_check is None:
|
|
62
|
+
return ProjectOpenDecision(
|
|
63
|
+
root, assessment, "choose", ("snapshot", "reinspect")
|
|
64
|
+
)
|
|
65
|
+
if assessment.source_check.state is SourceState.MISSING:
|
|
66
|
+
return ProjectOpenDecision(root, assessment, "choose", ("snapshot",))
|
|
67
|
+
return ProjectOpenDecision(root, assessment, "choose", ("snapshot", "rescan"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _check_decision(
|
|
71
|
+
location: StorageLocation, decision: ProjectOpenDecision
|
|
72
|
+
) -> SessionAssessment:
|
|
73
|
+
if not isinstance(decision, ProjectOpenDecision):
|
|
74
|
+
raise TypeError("A ProjectOpenDecision from inspection is required")
|
|
75
|
+
assessment = decision.assessment
|
|
76
|
+
if assessment.workspace_id != location.workspace_id:
|
|
77
|
+
raise GenerationConflictError("Opening decision belongs to another workspace")
|
|
78
|
+
if decision.storage_root != location.root.resolve():
|
|
79
|
+
raise GenerationConflictError(
|
|
80
|
+
"Opening decision belongs to another storage root"
|
|
81
|
+
)
|
|
82
|
+
if decision != _decision(location, assessment):
|
|
83
|
+
raise ValueError("Inconsistent project opening decision")
|
|
84
|
+
return assessment
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def inspect_opening(
|
|
88
|
+
location: StorageLocation,
|
|
89
|
+
project_id: str,
|
|
90
|
+
*,
|
|
91
|
+
requested_config: ScanConfig | None = None,
|
|
92
|
+
) -> ProjectOpenDecision:
|
|
93
|
+
"""Inspect one verified project, without opening a query session or scanning."""
|
|
94
|
+
return _decision(
|
|
95
|
+
location,
|
|
96
|
+
inspect_project(location, project_id, requested_config=requested_config),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@contextmanager
|
|
101
|
+
def enter_project(
|
|
102
|
+
location: StorageLocation,
|
|
103
|
+
project_id: str,
|
|
104
|
+
*,
|
|
105
|
+
requested_config: ScanConfig | None = None,
|
|
106
|
+
):
|
|
107
|
+
"""Open a fresh source as current; return a decision for every other state."""
|
|
108
|
+
decision = inspect_opening(location, project_id, requested_config=requested_config)
|
|
109
|
+
if decision.action != "open_current":
|
|
110
|
+
yield ProjectEntry(decision, None)
|
|
111
|
+
return
|
|
112
|
+
with ExitStack() as resources:
|
|
113
|
+
try:
|
|
114
|
+
session = resources.enter_context(
|
|
115
|
+
open_project(
|
|
116
|
+
location,
|
|
117
|
+
project_id,
|
|
118
|
+
requested_config=requested_config,
|
|
119
|
+
expected_generation_id=decision.assessment.generation_id,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
except SessionRefusedError as exc:
|
|
123
|
+
# The source changed between inspection and opening. Report the
|
|
124
|
+
# newer assessment instead of yielding a stale current session.
|
|
125
|
+
yield ProjectEntry(_decision(location, exc.assessment), None)
|
|
126
|
+
return
|
|
127
|
+
if session.assessment.source != decision.assessment.source:
|
|
128
|
+
raise GenerationConflictError("Selected project source binding changed")
|
|
129
|
+
yield ProjectEntry(_decision(location, session.assessment), session)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@contextmanager
|
|
133
|
+
def open_snapshot(location: StorageLocation, decision: ProjectOpenDecision):
|
|
134
|
+
"""Explicitly query the stored generation, carrying its warning and id."""
|
|
135
|
+
assessment = _check_decision(location, decision)
|
|
136
|
+
if "snapshot" not in decision.choices:
|
|
137
|
+
raise InputError("Snapshot is not an available choice for this assessment")
|
|
138
|
+
with open_project(
|
|
139
|
+
location,
|
|
140
|
+
assessment.project_id,
|
|
141
|
+
intent="snapshot",
|
|
142
|
+
expected_generation_id=assessment.generation_id,
|
|
143
|
+
) as session:
|
|
144
|
+
if session.assessment.source != assessment.source:
|
|
145
|
+
raise GenerationConflictError("Selected project source binding changed")
|
|
146
|
+
yield session
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@contextmanager
|
|
150
|
+
def rescan_from_decision(
|
|
151
|
+
location: StorageLocation,
|
|
152
|
+
decision: ProjectOpenDecision,
|
|
153
|
+
*,
|
|
154
|
+
config: ScanConfig | None = None,
|
|
155
|
+
value_limit: int = 10000,
|
|
156
|
+
intent: Literal["require_current", "snapshot"] = "require_current",
|
|
157
|
+
):
|
|
158
|
+
"""Explicitly rescan only the assessed project/source/generation."""
|
|
159
|
+
assessment = _check_decision(location, decision)
|
|
160
|
+
if "rescan" not in decision.choices:
|
|
161
|
+
raise InputError("Rescan is not an available choice for this assessment")
|
|
162
|
+
if decision.action == "resolve_config":
|
|
163
|
+
if config is None or not isinstance(config, ScanConfig):
|
|
164
|
+
raise InputError("A complete replacement ScanConfig is required")
|
|
165
|
+
validated = ScanConfig.model_validate(
|
|
166
|
+
config.model_dump(mode="json", by_alias=True)
|
|
167
|
+
)
|
|
168
|
+
if config_sha256(validated) != assessment.requested_config_sha256:
|
|
169
|
+
raise InputError("Replacement config differs from the inspected request")
|
|
170
|
+
with rescan_project(
|
|
171
|
+
location,
|
|
172
|
+
assessment.project_id,
|
|
173
|
+
expected_generation_id=assessment.generation_id,
|
|
174
|
+
expected_source=Path(assessment.source),
|
|
175
|
+
config=config,
|
|
176
|
+
value_limit=value_limit,
|
|
177
|
+
intent=intent,
|
|
178
|
+
) as result:
|
|
179
|
+
yield result
|