tabalyst 0.6.1__tar.gz → 0.6.2__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.1 → tabalyst-0.6.2}/MANIFEST.in +1 -0
- {tabalyst-0.6.1/src/tabalyst.egg-info → tabalyst-0.6.2}/PKG-INFO +31 -2
- {tabalyst-0.6.1 → tabalyst-0.6.2}/README.md +29 -1
- tabalyst-0.6.2/examples/README.md +144 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/pyproject.toml +3 -2
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/__init__.py +8 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/app.py +2 -0
- tabalyst-0.6.2/src/tabalyst/cli/generate.py +67 -0
- tabalyst-0.6.2/src/tabalyst/generate_anomalies.py +126 -0
- tabalyst-0.6.2/src/tabalyst/generate_csv.py +34 -0
- tabalyst-0.6.2/src/tabalyst/generate_definition.py +596 -0
- tabalyst-0.6.2/src/tabalyst/generate_examples/crm.yaml +70 -0
- tabalyst-0.6.2/src/tabalyst/generate_examples/insurance.yaml +71 -0
- tabalyst-0.6.2/src/tabalyst/generate_insurance.py +208 -0
- tabalyst-0.6.2/src/tabalyst/generate_references.py +221 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/SOURCES.md +27 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/street_directions_ca.csv +9 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/street_names_ca.csv +109 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/street_types_ca.csv +32 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/addresses/unit_types_ca.csv +12 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/contact/email_patterns.csv +12 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/contact/email_providers_ca.csv +10 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/contact/phone_area_codes_ca.csv +14 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/geography/cities_ca.csv +72 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/geography/postal_prefixes_ca.csv +72 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/geography/provinces_ca.csv +14 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/SOURCES.md +23 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/agencies.csv +21 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/branch_contract_weights.csv +13 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/claim_statuses.csv +5 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/contract_statuses.csv +5 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/contract_types.csv +5 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/payment_frequencies.csv +5 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/insurance/payment_methods.csv +5 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/people/family_names_ca.csv +136 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/people/first_names_en_ca.csv +81 -0
- tabalyst-0.6.2/src/tabalyst/generate_resources/people/first_names_fr_qc.csv +101 -0
- tabalyst-0.6.2/src/tabalyst/generate_service.py +367 -0
- tabalyst-0.6.2/src/tabalyst/generate_values.py +199 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/models.py +2 -2
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/models.py +2 -2
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/models.py +3 -3
- {tabalyst-0.6.1 → tabalyst-0.6.2/src/tabalyst.egg-info}/PKG-INFO +31 -2
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/SOURCES.txt +37 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/requires.txt +1 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_cli.py +2 -2
- tabalyst-0.6.2/tests/test_generate.py +368 -0
- tabalyst-0.6.2/tests/test_generate_crm.py +51 -0
- tabalyst-0.6.2/tests/test_generate_insurance.py +73 -0
- tabalyst-0.6.2/tests/test_generate_lot4.py +122 -0
- tabalyst-0.6.2/tests/test_generate_references.py +116 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_index.py +5 -3
- tabalyst-0.6.1/examples/README.md +0 -75
- {tabalyst-0.6.1 → tabalyst-0.6.2}/LICENSE +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/examples/config.json +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/examples/input/basic.csv +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/examples/input/insurance-customers.csv +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/setup.cfg +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/__main__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/_version.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/batch.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cache_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/cache.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/inspect.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/report.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/sample.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/scan.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/cli/terminal.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/config.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/errors.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/execution_log.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspect_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/choices.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/build.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/detect.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/excel_inspect/parameters.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/build.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/detect.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/json_inspect/parameters.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/persistence.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/inspector/resolution.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/option_notices.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/progress.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/project_open_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/project_scan_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_bounded_values.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_cache.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_codec.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_generation.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_locks.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_publication.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_queries.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_query_budget.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_recovery.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_retention.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_schema.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_session.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_staging.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_sync.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/_validation.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/catalog.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/freshness.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/identity.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/index.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/location.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/models.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/projects/store.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/report_config.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/report_profile.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/report_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/reporting.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/sampling.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/sampling_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scan_reuse.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scan_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/api.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/config.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/amount.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/base.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/boolean.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/currency.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/email.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/enumeration.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/ip.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/names.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/number.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/pattern.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/percentage.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/phone.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/postal.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/quantity.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/registry.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/shape.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/temporal.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/url.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/detectors/uuid.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/diagnostics.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/engine.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/exposure.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/field.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/identity.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/measures.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/normalization.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/observations.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/paths.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/__init__.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/base.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/csv_reader.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/excel_common.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/excel_reader.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/excel_table.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/json_common.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/json_reader.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/readers/jsonl_reader.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/records.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/structure.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/technical.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/values.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/scanner/workers.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/shared_scan_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/static/report.js +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/static/theme.css +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/templates/column.html +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst/templates/report.html +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/dependency_links.txt +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/entry_points.txt +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/src/tabalyst.egg-info/top_level.txt +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_all_collections.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_license_headers.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_option_notices.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_cache.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_database.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_generations.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_identity.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_location.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_open_service.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_queries.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_rescan.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_scan.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_sessions.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_project_store.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_public_api.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_reporting.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_sampling.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scan_command.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_amounts.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_batches.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_config.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_csv.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_email_url.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_exposure.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_paths.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_phone.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_postal.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_scanner_uuid_ip.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/tests/test_shared_scan_cli.py +0 -0
- {tabalyst-0.6.1 → tabalyst-0.6.2}/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.2
|
|
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
|
|
@@ -27,6 +27,7 @@ Requires-Dist: jinja2>=3.1
|
|
|
27
27
|
Requires-Dist: platformdirs>=4
|
|
28
28
|
Requires-Dist: pydantic>=2.11
|
|
29
29
|
Requires-Dist: python-calamine>=0.8
|
|
30
|
+
Requires-Dist: PyYAML>=6.0
|
|
30
31
|
Requires-Dist: typer>=0.12
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
@@ -48,7 +49,8 @@ Its first tool is **Tabalyst Report**. The current CSV implementation,
|
|
|
48
49
|
profile and a self-contained interactive HTML report.
|
|
49
50
|
**Tabalyst Scan** describes CSV, JSON, JSONL and Excel files in a scan document,
|
|
50
51
|
**Tabalyst Inspect** finds how to read a JSON, JSONL or Excel file, and
|
|
51
|
-
**Tabalyst Sample** creates smaller CSV files
|
|
52
|
+
**Tabalyst Sample** creates smaller CSV files by sampling, and **Tabalyst
|
|
53
|
+
Generate** creates synthetic CSV datasets.
|
|
52
54
|
|
|
53
55
|
Tabalyst is in beta. Its interfaces may still change while
|
|
54
56
|
the shared toolkit architecture is being established.
|
|
@@ -273,6 +275,33 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
|
|
|
273
275
|
only the requested sample, plus stratum counts, in memory. Existing outputs
|
|
274
276
|
require `--force`, and an input file is never overwritten.
|
|
275
277
|
|
|
278
|
+
## Tabalyst Generate
|
|
279
|
+
|
|
280
|
+
Generate synthetic CSV data from a versioned YAML definition. The packaged
|
|
281
|
+
`crm` example writes one contacts file; `insurance`
|
|
282
|
+
writes three branch files and a combined file:
|
|
283
|
+
|
|
284
|
+
```console
|
|
285
|
+
tabalyst generate --example crm -o crm-contacts.csv
|
|
286
|
+
tabalyst generate --example insurance --rows 3000 -d insurance-output/
|
|
287
|
+
tabalyst generate --example insurance --rows 3000 --anomaly-profile dirty_realistic -d insurance-dirty/
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
You can pass `examples/generate/input/crm.yaml` or
|
|
291
|
+
`examples/generate/input/insurance.yaml` instead of `--example`. A selected anomaly
|
|
292
|
+
profile writes an altered CSV, a cell change log and a summary beside the
|
|
293
|
+
untouched clean CSV. Insurance's profile changes only the combined file.
|
|
294
|
+
`--rows 3000` apportions 2,182, 273 and 545 rows to the three branches.
|
|
295
|
+
Existing outputs require `--force`.
|
|
296
|
+
|
|
297
|
+
Run `python examples/generate/generate_examples.py` to generate every YAML
|
|
298
|
+
from `examples/generate/input/` into ignored local output folders.
|
|
299
|
+
|
|
300
|
+
Insurance preserves the 33 source columns and the combined `filiale` column of
|
|
301
|
+
the existing public demo, but its generated rows are different. The branch date
|
|
302
|
+
and identifier formats are preserved; `filiale` is the last column in the new
|
|
303
|
+
combined CSV. The current public demo and report remain unchanged.
|
|
304
|
+
|
|
276
305
|
## Tabalyst Inspect
|
|
277
306
|
|
|
278
307
|
A JSON file can hold several arrays, and an Excel workbook several sheets.
|
|
@@ -10,7 +10,8 @@ Its first tool is **Tabalyst Report**. The current CSV implementation,
|
|
|
10
10
|
profile and a self-contained interactive HTML report.
|
|
11
11
|
**Tabalyst Scan** describes CSV, JSON, JSONL and Excel files in a scan document,
|
|
12
12
|
**Tabalyst Inspect** finds how to read a JSON, JSONL or Excel file, and
|
|
13
|
-
**Tabalyst Sample** creates smaller CSV files
|
|
13
|
+
**Tabalyst Sample** creates smaller CSV files by sampling, and **Tabalyst
|
|
14
|
+
Generate** creates synthetic CSV datasets.
|
|
14
15
|
|
|
15
16
|
Tabalyst is in beta. Its interfaces may still change while
|
|
16
17
|
the shared toolkit architecture is being established.
|
|
@@ -235,6 +236,33 @@ Sampling reads CSV records as a stream. Random and stratified sampling keep
|
|
|
235
236
|
only the requested sample, plus stratum counts, in memory. Existing outputs
|
|
236
237
|
require `--force`, and an input file is never overwritten.
|
|
237
238
|
|
|
239
|
+
## Tabalyst Generate
|
|
240
|
+
|
|
241
|
+
Generate synthetic CSV data from a versioned YAML definition. The packaged
|
|
242
|
+
`crm` example writes one contacts file; `insurance`
|
|
243
|
+
writes three branch files and a combined file:
|
|
244
|
+
|
|
245
|
+
```console
|
|
246
|
+
tabalyst generate --example crm -o crm-contacts.csv
|
|
247
|
+
tabalyst generate --example insurance --rows 3000 -d insurance-output/
|
|
248
|
+
tabalyst generate --example insurance --rows 3000 --anomaly-profile dirty_realistic -d insurance-dirty/
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
You can pass `examples/generate/input/crm.yaml` or
|
|
252
|
+
`examples/generate/input/insurance.yaml` instead of `--example`. A selected anomaly
|
|
253
|
+
profile writes an altered CSV, a cell change log and a summary beside the
|
|
254
|
+
untouched clean CSV. Insurance's profile changes only the combined file.
|
|
255
|
+
`--rows 3000` apportions 2,182, 273 and 545 rows to the three branches.
|
|
256
|
+
Existing outputs require `--force`.
|
|
257
|
+
|
|
258
|
+
Run `python examples/generate/generate_examples.py` to generate every YAML
|
|
259
|
+
from `examples/generate/input/` into ignored local output folders.
|
|
260
|
+
|
|
261
|
+
Insurance preserves the 33 source columns and the combined `filiale` column of
|
|
262
|
+
the existing public demo, but its generated rows are different. The branch date
|
|
263
|
+
and identifier formats are preserved; `filiale` is the last column in the new
|
|
264
|
+
combined CSV. The current public demo and report remain unchanged.
|
|
265
|
+
|
|
238
266
|
## Tabalyst Inspect
|
|
239
267
|
|
|
240
268
|
A JSON file can hold several arrays, and an Excel workbook several sheets.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Tabalyst examples
|
|
2
|
+
|
|
3
|
+
The examples are organized by input name so additional public datasets can be
|
|
4
|
+
added without mixing source data and generated artifacts.
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
examples/
|
|
8
|
+
|-- config.json
|
|
9
|
+
|-- generate/
|
|
10
|
+
| |-- generate_examples.py
|
|
11
|
+
| |-- input/ (four YAML definitions)
|
|
12
|
+
| `-- output/ (generated locally; ignored by Git)
|
|
13
|
+
|-- input/
|
|
14
|
+
| |-- basic.csv
|
|
15
|
+
| |-- insurance-customers.csv
|
|
16
|
+
| |-- orders.json
|
|
17
|
+
| |-- sales.xlsx
|
|
18
|
+
| `-- web-events.jsonl
|
|
19
|
+
`-- output/
|
|
20
|
+
|-- basic/
|
|
21
|
+
| |-- report.html
|
|
22
|
+
| |-- report.json
|
|
23
|
+
| `-- executions.json
|
|
24
|
+
|-- insurance-customers/
|
|
25
|
+
| |-- report.html
|
|
26
|
+
| |-- report.json
|
|
27
|
+
| |-- report/ (34 standalone column pages with --details)
|
|
28
|
+
| `-- executions.json
|
|
29
|
+
|-- orders/
|
|
30
|
+
| |-- report.html
|
|
31
|
+
| |-- report.json
|
|
32
|
+
| `-- executions.json
|
|
33
|
+
|-- sales/
|
|
34
|
+
| |-- report.html
|
|
35
|
+
| |-- report.json
|
|
36
|
+
| `-- executions.json
|
|
37
|
+
`-- web-events/
|
|
38
|
+
|-- report.html
|
|
39
|
+
|-- report.json
|
|
40
|
+
`-- executions.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`basic.csv` is a five-row smoke example. `insurance-customers.csv` contains 3,000
|
|
44
|
+
synthetic customer and contract records across 34 columns. Its people, contact
|
|
45
|
+
details, and contracts are fictional; email addresses use reserved `.example`
|
|
46
|
+
domains. `orders.json` is a synthetic shop export: 60 customers with nested
|
|
47
|
+
addresses, tags and orders. Its report has one dataset, `$.customers[]`, the only
|
|
48
|
+
collection of arrays of objects that Tabalyst Inspect finds in it.
|
|
49
|
+
`web-events.jsonl` is a synthetic web analytics log in JSON Lines: 300 events with
|
|
50
|
+
nested `user` and `device` objects, tags arrays, null values and an optional
|
|
51
|
+
field that appears late, plus three bad lines (a truncated record, a line that
|
|
52
|
+
is not JSON and a JSON array instead of an object). Its report is partial on
|
|
53
|
+
purpose: the three lines are excluded and counted, the 300 events are analyzed.
|
|
54
|
+
Its people and identifiers are fictional. `sales.xlsx` is a synthetic Excel
|
|
55
|
+
workbook: a sheet `Orders` of 120 fictional orders under two title lines, a
|
|
56
|
+
five-row `Regions` lookup sheet and a `Notes` sheet. Inspect selects `$.Orders`,
|
|
57
|
+
which has more than ten times the rows of `$.Regions`, so the report needs no
|
|
58
|
+
choice. The workbook is written by `make_sales_workbook.py`, with the same
|
|
59
|
+
bytes on every run; it needs `pip install xlsxwriter` and is only to run when the
|
|
60
|
+
input itself must be regenerated.
|
|
61
|
+
|
|
62
|
+
From the repository root, regenerate the outputs with:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
tabalyst report examples/input/basic.csv -o examples/output/basic/report.html --config examples/config.json --force
|
|
66
|
+
tabalyst report examples/input/insurance-customers.csv -o examples/output/insurance-customers/report.html --config examples/config.json --details --force
|
|
67
|
+
tabalyst report examples/input/orders.json -o examples/output/orders/report.html --config examples/config.json --force
|
|
68
|
+
tabalyst report examples/input/web-events.jsonl -o examples/output/web-events/report.html --config examples/config.json --force
|
|
69
|
+
tabalyst report examples/input/sales.xlsx -o examples/output/sales/report.html --config examples/config.json --force
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Every output folder is independent and contains its own HTML report, canonical
|
|
73
|
+
JSON profile and cumulative `executions.json` history. The insurance example
|
|
74
|
+
also has standalone HTML pages under `report/`. Delete an output folder
|
|
75
|
+
before regenerating when a fresh one-entry execution history is required.
|
|
76
|
+
|
|
77
|
+
The input datasets and generated output are public repository examples. The wheel
|
|
78
|
+
contains the runtime package, report resources, and packaged copies of the
|
|
79
|
+
`examples/generate/input/` YAML definitions. CRM and Insurance are local development
|
|
80
|
+
demos. Generate is under development
|
|
81
|
+
and is not advertised as a released feature. The CRM data is synthetic, with
|
|
82
|
+
reserved `.example` domains and fictional phone numbers. The configured 20,000
|
|
83
|
+
rows, generic account names and CRM audit story were approved for the example
|
|
84
|
+
on 2026-10-05. Generate remains local development work until a release.
|
|
85
|
+
|
|
86
|
+
To review the CRM export outside the checkout:
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
tabalyst generate --example crm --rows 500 --anomaly-profile dirty_realistic -d OUTPUT_DIR
|
|
90
|
+
tabalyst scan OUTPUT_DIR/crm-contacts.dirty_realistic.csv -o OUTPUT_DIR/scan.json
|
|
91
|
+
tabalyst report OUTPUT_DIR/scan.json --scan -o OUTPUT_DIR/report.html
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Scan and Report expose missing values, fragmented categories and normalized
|
|
95
|
+
variants. The mutation summary counts requested, applied and skipped changes.
|
|
96
|
+
Neither tool currently diagnoses cross-column CRM rules such as stage versus
|
|
97
|
+
probability.
|
|
98
|
+
|
|
99
|
+
`examples/generate/input/common-references.yaml` is a lot 3R development fixture.
|
|
100
|
+
It generates coherent synthetic Canadian person, address and contact fields
|
|
101
|
+
from packaged references. Its prefixes and area codes are synthetic, email
|
|
102
|
+
domains end in `.example`, and phone line numbers use reserved `555-01xx`.
|
|
103
|
+
The Insurance example is now complete in `examples/generate/input/insurance.yaml`
|
|
104
|
+
and the packaged `--example insurance` copy. Its seven domain tables are in
|
|
105
|
+
`src/tabalyst/generate_resources/insurance/`; their provenance is in
|
|
106
|
+
`insurance/SOURCES.md`. To review 3,000 rows outside the checkout:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
tabalyst generate --example insurance --rows 3000 --anomaly-profile dirty_realistic -d OUTPUT_DIR
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This creates clean files for Principal, Ontario and Quebec, their clean union,
|
|
113
|
+
and three anomaly artifacts for that union. The 2,182/273/545 branch allocation
|
|
114
|
+
comes from the declared 200,000/25,000/50,000 weights. The new clean CSV has
|
|
115
|
+
the same 34 column names as the existing public demo, but places `filiale` last
|
|
116
|
+
and has different rows and identifiers. Do not replace
|
|
117
|
+
`examples/input/insurance-customers.csv` or its report without a separate
|
|
118
|
+
decision to change the published demo.
|
|
119
|
+
|
|
120
|
+
`examples/generate/input/cohorts-anomalies.yaml` is a lot 4 development fixture. It
|
|
121
|
+
shows two normal status conventions in the clean CSV and a separate `dirty`
|
|
122
|
+
profile that writes an altered CSV, cell-level change log and summary. Run it
|
|
123
|
+
with `tabalyst generate examples/generate/input/cohorts-anomalies.yaml --anomaly-profile dirty -d OUTPUT_DIR`.
|
|
124
|
+
Keep `OUTPUT_DIR` outside the repository for one-off local trials. The packaged
|
|
125
|
+
Insurance YAML is the same complete example as the visible file.
|
|
126
|
+
|
|
127
|
+
## Regenerate all Generate examples
|
|
128
|
+
|
|
129
|
+
Run the script from the repository root with an editable installation of
|
|
130
|
+
Tabalyst. It reads every YAML in `examples/generate/input/` and writes into
|
|
131
|
+
`examples/generate/output/<definition>/`. The output folder is ignored by Git.
|
|
132
|
+
By default it uses each YAML's declared row counts and generates every declared
|
|
133
|
+
anomaly profile along with its clean CSV. Insurance therefore writes 275,000
|
|
134
|
+
combined rows; allow enough disk space for the clean and altered copies.
|
|
135
|
+
|
|
136
|
+
```console
|
|
137
|
+
python examples/generate/generate_examples.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Existing outputs are protected; pass `--force` to replace generated files.
|
|
141
|
+
For a quick review run, `--rows 30` overrides each definition's row count,
|
|
142
|
+
and `--output-dir PATH` writes outside the checkout. `--clean-only` skips the
|
|
143
|
+
profile passes. A definition with several profiles gets one subfolder per
|
|
144
|
+
profile so its clean files cannot collide.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tabalyst"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
8
8
|
description = "Open-source toolkit for understanding and working with unfamiliar data."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -40,6 +40,7 @@ dependencies = [
|
|
|
40
40
|
"platformdirs>=4",
|
|
41
41
|
"pydantic>=2.11",
|
|
42
42
|
"python-calamine>=0.8",
|
|
43
|
+
"PyYAML>=6.0",
|
|
43
44
|
"typer>=0.12",
|
|
44
45
|
]
|
|
45
46
|
|
|
@@ -59,7 +60,7 @@ tabalyst = "tabalyst.cli:main"
|
|
|
59
60
|
where = ["src"]
|
|
60
61
|
|
|
61
62
|
[tool.setuptools.package-data]
|
|
62
|
-
tabalyst = ["templates/*.html", "static/*.css", "static/*.js"]
|
|
63
|
+
tabalyst = ["templates/*.html", "static/*.css", "static/*.js", "generate_examples/*.yaml", "generate_resources/**/*.csv", "generate_resources/*.md"]
|
|
63
64
|
|
|
64
65
|
[project.urls]
|
|
65
66
|
Homepage = "https://github.com/loribel-labs/tabalyst"
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
from tabalyst._version import __version__
|
|
8
8
|
from tabalyst.errors import ConfigurationError, InputError, ReportError, TabalystError
|
|
9
|
+
from tabalyst.generate_service import GeneratePlan as GeneratePlan
|
|
10
|
+
from tabalyst.generate_service import GenerateResult as GenerateResult
|
|
11
|
+
from tabalyst.generate_service import build_generate_plan as build_generate_plan
|
|
12
|
+
from tabalyst.generate_service import generate_datasets as generate_datasets
|
|
9
13
|
from tabalyst.inspect_service import InspectResult as InspectResult
|
|
10
14
|
from tabalyst.inspect_service import generate_inspections as generate_inspections
|
|
11
15
|
from tabalyst.inspect_service import inspect as inspect
|
|
@@ -26,6 +30,8 @@ from tabalyst.service import analyze_scan as analyze_scan
|
|
|
26
30
|
|
|
27
31
|
__all__ = [
|
|
28
32
|
"ConfigurationError",
|
|
33
|
+
"GeneratePlan",
|
|
34
|
+
"GenerateResult",
|
|
29
35
|
"InputError",
|
|
30
36
|
"InspectResult",
|
|
31
37
|
"ReportError",
|
|
@@ -34,6 +40,8 @@ __all__ = [
|
|
|
34
40
|
"TabalystError",
|
|
35
41
|
"__version__",
|
|
36
42
|
"analyze",
|
|
43
|
+
"build_generate_plan",
|
|
44
|
+
"generate_datasets",
|
|
37
45
|
"generate_inspections",
|
|
38
46
|
"generate_reports",
|
|
39
47
|
"generate_samples",
|
|
@@ -10,6 +10,7 @@ import typer
|
|
|
10
10
|
|
|
11
11
|
from tabalyst import __version__
|
|
12
12
|
from tabalyst.cli.cache import cache_app
|
|
13
|
+
from tabalyst.cli.generate import generate_command
|
|
13
14
|
from tabalyst.cli.inspect import inspect_command
|
|
14
15
|
from tabalyst.cli.report import report_command
|
|
15
16
|
from tabalyst.cli.sample import sample_command
|
|
@@ -45,6 +46,7 @@ def root(
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
app.command("inspect")(inspect_command)
|
|
49
|
+
app.command("generate")(generate_command)
|
|
48
50
|
app.command("report")(report_command)
|
|
49
51
|
app.command("sample")(sample_command)
|
|
50
52
|
app.command("scan")(scan_command)
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
"""CLI adapter for Tabalyst Generate."""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from tabalyst.cli.terminal import error_exit_code
|
|
13
|
+
from tabalyst.errors import TabalystError
|
|
14
|
+
from tabalyst.generate_service import build_generate_plan, generate_datasets
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_command(
|
|
18
|
+
definition: Annotated[
|
|
19
|
+
Path | None,
|
|
20
|
+
typer.Argument(help="YAML Generate definition."),
|
|
21
|
+
] = None,
|
|
22
|
+
example: Annotated[
|
|
23
|
+
str | None,
|
|
24
|
+
typer.Option("--example", help="Packaged example: crm or insurance."),
|
|
25
|
+
] = None,
|
|
26
|
+
output: Annotated[
|
|
27
|
+
Path | None,
|
|
28
|
+
typer.Option("--output", "-o", help="One clean CSV file."),
|
|
29
|
+
] = None,
|
|
30
|
+
output_dir: Annotated[
|
|
31
|
+
Path | None,
|
|
32
|
+
typer.Option("--output-dir", "-d", help="Directory for all artifacts."),
|
|
33
|
+
] = None,
|
|
34
|
+
rows: Annotated[
|
|
35
|
+
int | None,
|
|
36
|
+
typer.Option("--rows", help="Number of generated rows."),
|
|
37
|
+
] = None,
|
|
38
|
+
seed: Annotated[
|
|
39
|
+
int | None,
|
|
40
|
+
typer.Option("--seed", help="Root random seed."),
|
|
41
|
+
] = None,
|
|
42
|
+
as_of_date: Annotated[
|
|
43
|
+
str | None,
|
|
44
|
+
typer.Option("--as-of-date", help="Reference date (YYYY-MM-DD)."),
|
|
45
|
+
] = None,
|
|
46
|
+
anomaly_profile: Annotated[
|
|
47
|
+
str | None,
|
|
48
|
+
typer.Option("--anomaly-profile", help="Selected anomaly profile."),
|
|
49
|
+
] = None,
|
|
50
|
+
force: Annotated[
|
|
51
|
+
bool,
|
|
52
|
+
typer.Option("--force", "-f", help="Replace existing artifacts."),
|
|
53
|
+
] = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Validate and generate datasets from a YAML definition."""
|
|
56
|
+
try:
|
|
57
|
+
plan = build_generate_plan(
|
|
58
|
+
definition, example=example, output=output, output_dir=output_dir,
|
|
59
|
+
rows=rows, seed=seed, as_of_date=as_of_date,
|
|
60
|
+
anomaly_profile=anomaly_profile, force=force,
|
|
61
|
+
)
|
|
62
|
+
result = generate_datasets(plan)
|
|
63
|
+
except TabalystError as exc:
|
|
64
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
65
|
+
raise typer.Exit(error_exit_code(exc)) from exc
|
|
66
|
+
for path in result.paths:
|
|
67
|
+
typer.echo(f"Created: {path}", err=True)
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
"""Measured, streaming mutation pass over a staged clean Generate CSV."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import random
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from tabalyst.generate_values import _stream
|
|
15
|
+
|
|
16
|
+
CHANGE_FIELDS = ["event_id", "dataset", "profile", "row_number", "operation_index",
|
|
17
|
+
"kind", "column", "before", "after", "cause"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _mutate(row: dict[str, str], operation: dict, history: dict[str, list[str]],
|
|
21
|
+
rng: random.Random) -> dict[str, str]:
|
|
22
|
+
column = operation["column"]
|
|
23
|
+
before = row[column]
|
|
24
|
+
kind = operation["kind"]
|
|
25
|
+
if kind == "set_missing":
|
|
26
|
+
return {column: ""} if before else {}
|
|
27
|
+
if kind == "replace":
|
|
28
|
+
return {column: operation["value"]} if operation["value"] != before else {}
|
|
29
|
+
if kind == "duplicate":
|
|
30
|
+
choices = [value for value in history[column] if value and value != before]
|
|
31
|
+
return {column: rng.choice(choices)} if choices else {}
|
|
32
|
+
if kind == "swap":
|
|
33
|
+
other = operation["value"]
|
|
34
|
+
return {column: row[other], other: before} if before != row[other] else {}
|
|
35
|
+
transforms = {
|
|
36
|
+
"upper": str.upper,
|
|
37
|
+
"lower": str.lower,
|
|
38
|
+
"strip": str.strip,
|
|
39
|
+
"date_slash": lambda value: value.replace("-", "/"),
|
|
40
|
+
"phone_spaces": lambda value: value.replace("-", " "),
|
|
41
|
+
}
|
|
42
|
+
after = transforms[operation["value"]](before)
|
|
43
|
+
return {column: after} if before and after != before else {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def inject_anomalies(clean: Path, altered: Path, changes: Path, *,
|
|
47
|
+
dataset: str, profile: dict, seed: int, count: int,
|
|
48
|
+
delimiter: str) -> dict:
|
|
49
|
+
"""Write altered CSV and cell log; return requested and actual counts."""
|
|
50
|
+
operations = profile["operations"]
|
|
51
|
+
requested = [round(count * operation["rate"]) for operation in operations]
|
|
52
|
+
remaining = requested.copy()
|
|
53
|
+
selection_rngs = [_stream(seed, dataset, f"{profile['id']}:{index}",
|
|
54
|
+
"anomaly_selection") for index in range(len(operations))]
|
|
55
|
+
mutation_rngs = [_stream(seed, dataset, f"{profile['id']}:{index}", "anomaly_value")
|
|
56
|
+
for index in range(len(operations))]
|
|
57
|
+
applied: Counter[int] = Counter()
|
|
58
|
+
skipped: Counter[int] = Counter()
|
|
59
|
+
history: dict[str, list[str]] = {}
|
|
60
|
+
affected_rows = 0
|
|
61
|
+
changed_cells = 0
|
|
62
|
+
events = 0
|
|
63
|
+
with (clean.open("r", encoding="utf-8", newline="") as source,
|
|
64
|
+
altered.open("w", encoding="utf-8", newline="") as dirty,
|
|
65
|
+
changes.open("w", encoding="utf-8", newline="") as journal):
|
|
66
|
+
reader = csv.DictReader(source, delimiter=delimiter)
|
|
67
|
+
fields = reader.fieldnames or []
|
|
68
|
+
history = {field: [] for field in fields}
|
|
69
|
+
writer = csv.DictWriter(dirty, fieldnames=fields, delimiter=delimiter,
|
|
70
|
+
lineterminator="\n", extrasaction="raise")
|
|
71
|
+
log = csv.DictWriter(journal, fieldnames=CHANGE_FIELDS, lineterminator="\n")
|
|
72
|
+
writer.writeheader()
|
|
73
|
+
log.writeheader()
|
|
74
|
+
observed = 0
|
|
75
|
+
for row_index, clean_row in enumerate(reader):
|
|
76
|
+
observed += 1
|
|
77
|
+
row = clean_row.copy()
|
|
78
|
+
changed = False
|
|
79
|
+
for index, operation in enumerate(operations):
|
|
80
|
+
rows_left = count - row_index
|
|
81
|
+
selected = (remaining[index] == rows_left or
|
|
82
|
+
(remaining[index] > 0 and
|
|
83
|
+
selection_rngs[index].random() < remaining[index] / rows_left))
|
|
84
|
+
if not selected:
|
|
85
|
+
continue
|
|
86
|
+
remaining[index] -= 1
|
|
87
|
+
when = operation.get("when", {})
|
|
88
|
+
if any(row.get(field) != value for field, value in when.items()):
|
|
89
|
+
skipped[index] += 1
|
|
90
|
+
continue
|
|
91
|
+
updates = _mutate(row, operation, history, mutation_rngs[index])
|
|
92
|
+
if not updates:
|
|
93
|
+
skipped[index] += 1
|
|
94
|
+
continue
|
|
95
|
+
events += 1
|
|
96
|
+
for column, after in updates.items():
|
|
97
|
+
before = row[column]
|
|
98
|
+
row[column] = after
|
|
99
|
+
changed_cells += 1
|
|
100
|
+
log.writerow({"event_id": events, "dataset": dataset,
|
|
101
|
+
"profile": profile["id"], "row_number": row_index + 2,
|
|
102
|
+
"operation_index": index + 1, "kind": operation["kind"],
|
|
103
|
+
"column": column, "before": before, "after": after,
|
|
104
|
+
"cause": f"{profile['id']}.operations[{index}]"})
|
|
105
|
+
applied[index] += 1
|
|
106
|
+
changed = True
|
|
107
|
+
writer.writerow(row)
|
|
108
|
+
affected_rows += int(changed)
|
|
109
|
+
for field, value in clean_row.items():
|
|
110
|
+
if value and len(history[field]) < 1000:
|
|
111
|
+
history[field].append(value)
|
|
112
|
+
if observed != count:
|
|
113
|
+
raise ValueError(f"Clean row count changed for {dataset}: {observed} != {count}")
|
|
114
|
+
return {
|
|
115
|
+
"dataset": dataset, "profile": profile["id"], "seed": seed,
|
|
116
|
+
"total_rows": count, "affected_rows": affected_rows,
|
|
117
|
+
"affected_row_rate": affected_rows / count if count else 0.0,
|
|
118
|
+
"events": events, "changed_cells": changed_cells,
|
|
119
|
+
"operations": [
|
|
120
|
+
{"index": index + 1, "kind": operation["kind"], "column": operation["column"],
|
|
121
|
+
"rate": operation["rate"], "requested_rows": requested[index],
|
|
122
|
+
"applied_rows": applied[index], "skipped_rows": skipped[index],
|
|
123
|
+
"applied_rate": applied[index] / count if count else 0.0}
|
|
124
|
+
for index, operation in enumerate(operations)
|
|
125
|
+
],
|
|
126
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
"""Streaming CSV output adapted from dataset_factory.exporters.csv."""
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_csv(
|
|
13
|
+
path: Path,
|
|
14
|
+
fieldnames: list[str],
|
|
15
|
+
rows: Iterable[dict[str, str]],
|
|
16
|
+
*,
|
|
17
|
+
delimiter: str = ",",
|
|
18
|
+
) -> int:
|
|
19
|
+
"""Write a staged CSV using stable UTF-8/LF and strict field names."""
|
|
20
|
+
count = 0
|
|
21
|
+
with path.open("w", encoding="utf-8", newline="") as stream:
|
|
22
|
+
writer = csv.DictWriter(
|
|
23
|
+
stream,
|
|
24
|
+
fieldnames=fieldnames,
|
|
25
|
+
extrasaction="raise",
|
|
26
|
+
lineterminator="\n",
|
|
27
|
+
delimiter=delimiter,
|
|
28
|
+
)
|
|
29
|
+
writer.writeheader()
|
|
30
|
+
for row in rows:
|
|
31
|
+
writer.writerow(row)
|
|
32
|
+
count += 1
|
|
33
|
+
stream.flush()
|
|
34
|
+
return count
|