truthound 1.0.8__py3-none-any.whl
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.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
"""Reporter output validation and schema support.
|
|
2
|
+
|
|
3
|
+
This module provides schema validation for reporter outputs to ensure
|
|
4
|
+
consistent, well-formed reports across different reporter implementations.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from truthound.reporters.sdk.schema import (
|
|
8
|
+
... ReportSchema,
|
|
9
|
+
... JSONSchema,
|
|
10
|
+
... validate_output,
|
|
11
|
+
... register_schema,
|
|
12
|
+
... )
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Define a schema for your reporter
|
|
15
|
+
>>> schema = JSONSchema({
|
|
16
|
+
... "type": "object",
|
|
17
|
+
... "properties": {
|
|
18
|
+
... "summary": {"type": "object"},
|
|
19
|
+
... "results": {"type": "array"},
|
|
20
|
+
... },
|
|
21
|
+
... "required": ["summary", "results"],
|
|
22
|
+
... })
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Register for automatic validation
|
|
25
|
+
>>> register_schema("my_reporter", schema)
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Validate output
|
|
28
|
+
>>> result = validate_output(output, schema)
|
|
29
|
+
>>> if not result.valid:
|
|
30
|
+
... print(f"Validation errors: {result.errors}")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import re
|
|
37
|
+
from abc import ABC, abstractmethod
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from enum import Enum
|
|
40
|
+
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
|
41
|
+
from xml.etree import ElementTree
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Core classes
|
|
45
|
+
"ReportSchema",
|
|
46
|
+
"JSONSchema",
|
|
47
|
+
"XMLSchema",
|
|
48
|
+
"CSVSchema",
|
|
49
|
+
"TextSchema",
|
|
50
|
+
# Validation
|
|
51
|
+
"ValidationResult",
|
|
52
|
+
"ValidationError",
|
|
53
|
+
"SchemaError",
|
|
54
|
+
# Functions
|
|
55
|
+
"validate_output",
|
|
56
|
+
"register_schema",
|
|
57
|
+
"get_schema",
|
|
58
|
+
"unregister_schema",
|
|
59
|
+
# Decorators
|
|
60
|
+
"validate_reporter_output",
|
|
61
|
+
# Utilities
|
|
62
|
+
"infer_schema",
|
|
63
|
+
"merge_schemas",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SchemaError(Exception):
|
|
68
|
+
"""Exception raised for schema-related errors."""
|
|
69
|
+
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ValidationError(Exception):
|
|
74
|
+
"""Exception raised when validation fails."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
message: str,
|
|
79
|
+
path: Optional[str] = None,
|
|
80
|
+
value: Any = None,
|
|
81
|
+
expected: Optional[str] = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
self.message = message
|
|
84
|
+
self.path = path
|
|
85
|
+
self.value = value
|
|
86
|
+
self.expected = expected
|
|
87
|
+
super().__init__(self._format_message())
|
|
88
|
+
|
|
89
|
+
def _format_message(self) -> str:
|
|
90
|
+
parts = [self.message]
|
|
91
|
+
if self.path:
|
|
92
|
+
parts.append(f"at path '{self.path}'")
|
|
93
|
+
if self.expected:
|
|
94
|
+
parts.append(f"(expected: {self.expected})")
|
|
95
|
+
return " ".join(parts)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ValidationResult:
|
|
100
|
+
"""Result of schema validation."""
|
|
101
|
+
|
|
102
|
+
valid: bool
|
|
103
|
+
errors: List[ValidationError] = field(default_factory=list)
|
|
104
|
+
warnings: List[str] = field(default_factory=list)
|
|
105
|
+
schema_name: Optional[str] = None
|
|
106
|
+
checked_at: Optional[str] = None
|
|
107
|
+
|
|
108
|
+
def raise_if_invalid(self) -> None:
|
|
109
|
+
"""Raise SchemaError if validation failed."""
|
|
110
|
+
if not self.valid:
|
|
111
|
+
error_messages = [str(e) for e in self.errors]
|
|
112
|
+
raise SchemaError(
|
|
113
|
+
f"Validation failed with {len(self.errors)} error(s): "
|
|
114
|
+
+ "; ".join(error_messages)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
118
|
+
"""Convert to dictionary representation."""
|
|
119
|
+
return {
|
|
120
|
+
"valid": self.valid,
|
|
121
|
+
"errors": [
|
|
122
|
+
{
|
|
123
|
+
"message": e.message,
|
|
124
|
+
"path": e.path,
|
|
125
|
+
"value": repr(e.value) if e.value is not None else None,
|
|
126
|
+
"expected": e.expected,
|
|
127
|
+
}
|
|
128
|
+
for e in self.errors
|
|
129
|
+
],
|
|
130
|
+
"warnings": self.warnings,
|
|
131
|
+
"schema_name": self.schema_name,
|
|
132
|
+
"checked_at": self.checked_at,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ReportSchema(ABC):
|
|
137
|
+
"""Base class for report schemas.
|
|
138
|
+
|
|
139
|
+
Subclass this to create custom schema validators for your
|
|
140
|
+
reporter output format.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> class MyCustomSchema(ReportSchema):
|
|
144
|
+
... def validate(self, output: Any) -> ValidationResult:
|
|
145
|
+
... errors = []
|
|
146
|
+
... if not isinstance(output, dict):
|
|
147
|
+
... errors.append(ValidationError("Expected dictionary"))
|
|
148
|
+
... return ValidationResult(valid=len(errors) == 0, errors=errors)
|
|
149
|
+
...
|
|
150
|
+
... def to_dict(self) -> Dict[str, Any]:
|
|
151
|
+
... return {"type": "custom"}
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, name: Optional[str] = None) -> None:
|
|
155
|
+
self.name = name or self.__class__.__name__
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def validate(self, output: Any) -> ValidationResult:
|
|
159
|
+
"""Validate output against this schema.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
output: The output to validate.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
ValidationResult with validation status and any errors.
|
|
166
|
+
"""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
171
|
+
"""Convert schema to dictionary representation."""
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def __repr__(self) -> str:
|
|
175
|
+
return f"{self.__class__.__name__}(name={self.name!r})"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class JSONSchema(ReportSchema):
|
|
179
|
+
"""JSON Schema validator for reporter output.
|
|
180
|
+
|
|
181
|
+
Supports a subset of JSON Schema draft-07 for validating
|
|
182
|
+
JSON/dictionary output from reporters.
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
>>> schema = JSONSchema({
|
|
186
|
+
... "type": "object",
|
|
187
|
+
... "properties": {
|
|
188
|
+
... "name": {"type": "string"},
|
|
189
|
+
... "count": {"type": "integer", "minimum": 0},
|
|
190
|
+
... },
|
|
191
|
+
... "required": ["name"],
|
|
192
|
+
... })
|
|
193
|
+
>>> result = schema.validate({"name": "test", "count": 5})
|
|
194
|
+
>>> print(result.valid) # True
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
schema: Dict[str, Any],
|
|
200
|
+
name: Optional[str] = None,
|
|
201
|
+
strict: bool = False,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Initialize JSON Schema validator.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
schema: JSON Schema definition.
|
|
207
|
+
name: Optional schema name.
|
|
208
|
+
strict: If True, disallow additional properties by default.
|
|
209
|
+
"""
|
|
210
|
+
super().__init__(name)
|
|
211
|
+
self.schema = schema
|
|
212
|
+
self.strict = strict
|
|
213
|
+
self._type_validators: Dict[str, Callable] = {
|
|
214
|
+
"string": lambda v: isinstance(v, str),
|
|
215
|
+
"integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
|
|
216
|
+
"number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
|
|
217
|
+
"boolean": lambda v: isinstance(v, bool),
|
|
218
|
+
"array": lambda v: isinstance(v, list),
|
|
219
|
+
"object": lambda v: isinstance(v, dict),
|
|
220
|
+
"null": lambda v: v is None,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def validate(self, output: Any) -> ValidationResult:
|
|
224
|
+
"""Validate output against JSON Schema."""
|
|
225
|
+
from datetime import datetime
|
|
226
|
+
|
|
227
|
+
errors: List[ValidationError] = []
|
|
228
|
+
warnings: List[str] = []
|
|
229
|
+
|
|
230
|
+
self._validate_value(output, self.schema, "", errors, warnings)
|
|
231
|
+
|
|
232
|
+
return ValidationResult(
|
|
233
|
+
valid=len(errors) == 0,
|
|
234
|
+
errors=errors,
|
|
235
|
+
warnings=warnings,
|
|
236
|
+
schema_name=self.name,
|
|
237
|
+
checked_at=datetime.now().isoformat(),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _validate_value(
|
|
241
|
+
self,
|
|
242
|
+
value: Any,
|
|
243
|
+
schema: Dict[str, Any],
|
|
244
|
+
path: str,
|
|
245
|
+
errors: List[ValidationError],
|
|
246
|
+
warnings: List[str],
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Recursively validate a value against schema."""
|
|
249
|
+
# Handle type validation
|
|
250
|
+
if "type" in schema:
|
|
251
|
+
expected_types = schema["type"]
|
|
252
|
+
if isinstance(expected_types, str):
|
|
253
|
+
expected_types = [expected_types]
|
|
254
|
+
|
|
255
|
+
type_valid = any(
|
|
256
|
+
self._type_validators.get(t, lambda v: False)(value)
|
|
257
|
+
for t in expected_types
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if not type_valid:
|
|
261
|
+
errors.append(
|
|
262
|
+
ValidationError(
|
|
263
|
+
f"Invalid type: got {type(value).__name__}",
|
|
264
|
+
path=path or "$",
|
|
265
|
+
value=value,
|
|
266
|
+
expected=", ".join(expected_types),
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# Handle enum validation
|
|
272
|
+
if "enum" in schema:
|
|
273
|
+
if value not in schema["enum"]:
|
|
274
|
+
errors.append(
|
|
275
|
+
ValidationError(
|
|
276
|
+
f"Value not in enum",
|
|
277
|
+
path=path or "$",
|
|
278
|
+
value=value,
|
|
279
|
+
expected=str(schema["enum"]),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Handle const validation
|
|
284
|
+
if "const" in schema:
|
|
285
|
+
if value != schema["const"]:
|
|
286
|
+
errors.append(
|
|
287
|
+
ValidationError(
|
|
288
|
+
f"Value does not match const",
|
|
289
|
+
path=path or "$",
|
|
290
|
+
value=value,
|
|
291
|
+
expected=repr(schema["const"]),
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Handle string-specific validations
|
|
296
|
+
if isinstance(value, str):
|
|
297
|
+
self._validate_string(value, schema, path, errors)
|
|
298
|
+
|
|
299
|
+
# Handle number-specific validations
|
|
300
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
301
|
+
self._validate_number(value, schema, path, errors)
|
|
302
|
+
|
|
303
|
+
# Handle array validations
|
|
304
|
+
if isinstance(value, list):
|
|
305
|
+
self._validate_array(value, schema, path, errors, warnings)
|
|
306
|
+
|
|
307
|
+
# Handle object validations
|
|
308
|
+
if isinstance(value, dict):
|
|
309
|
+
self._validate_object(value, schema, path, errors, warnings)
|
|
310
|
+
|
|
311
|
+
def _validate_string(
|
|
312
|
+
self,
|
|
313
|
+
value: str,
|
|
314
|
+
schema: Dict[str, Any],
|
|
315
|
+
path: str,
|
|
316
|
+
errors: List[ValidationError],
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Validate string-specific constraints."""
|
|
319
|
+
if "minLength" in schema and len(value) < schema["minLength"]:
|
|
320
|
+
errors.append(
|
|
321
|
+
ValidationError(
|
|
322
|
+
f"String too short (length: {len(value)})",
|
|
323
|
+
path=path,
|
|
324
|
+
value=value,
|
|
325
|
+
expected=f"minLength: {schema['minLength']}",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if "maxLength" in schema and len(value) > schema["maxLength"]:
|
|
330
|
+
errors.append(
|
|
331
|
+
ValidationError(
|
|
332
|
+
f"String too long (length: {len(value)})",
|
|
333
|
+
path=path,
|
|
334
|
+
value=value,
|
|
335
|
+
expected=f"maxLength: {schema['maxLength']}",
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if "pattern" in schema:
|
|
340
|
+
if not re.match(schema["pattern"], value):
|
|
341
|
+
errors.append(
|
|
342
|
+
ValidationError(
|
|
343
|
+
f"String does not match pattern",
|
|
344
|
+
path=path,
|
|
345
|
+
value=value,
|
|
346
|
+
expected=f"pattern: {schema['pattern']}",
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if "format" in schema:
|
|
351
|
+
self._validate_format(value, schema["format"], path, errors)
|
|
352
|
+
|
|
353
|
+
def _validate_format(
|
|
354
|
+
self,
|
|
355
|
+
value: str,
|
|
356
|
+
format_type: str,
|
|
357
|
+
path: str,
|
|
358
|
+
errors: List[ValidationError],
|
|
359
|
+
) -> None:
|
|
360
|
+
"""Validate string format."""
|
|
361
|
+
format_patterns = {
|
|
362
|
+
"email": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
|
363
|
+
"uri": r"^https?://[^\s]+$",
|
|
364
|
+
"date": r"^\d{4}-\d{2}-\d{2}$",
|
|
365
|
+
"time": r"^\d{2}:\d{2}:\d{2}$",
|
|
366
|
+
"date-time": r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}",
|
|
367
|
+
"uuid": r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
|
368
|
+
"ipv4": r"^(\d{1,3}\.){3}\d{1,3}$",
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if format_type in format_patterns:
|
|
372
|
+
if not re.match(format_patterns[format_type], value):
|
|
373
|
+
errors.append(
|
|
374
|
+
ValidationError(
|
|
375
|
+
f"String does not match format",
|
|
376
|
+
path=path,
|
|
377
|
+
value=value,
|
|
378
|
+
expected=f"format: {format_type}",
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def _validate_number(
|
|
383
|
+
self,
|
|
384
|
+
value: Union[int, float],
|
|
385
|
+
schema: Dict[str, Any],
|
|
386
|
+
path: str,
|
|
387
|
+
errors: List[ValidationError],
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Validate number-specific constraints."""
|
|
390
|
+
if "minimum" in schema and value < schema["minimum"]:
|
|
391
|
+
errors.append(
|
|
392
|
+
ValidationError(
|
|
393
|
+
f"Number below minimum",
|
|
394
|
+
path=path,
|
|
395
|
+
value=value,
|
|
396
|
+
expected=f"minimum: {schema['minimum']}",
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if "maximum" in schema and value > schema["maximum"]:
|
|
401
|
+
errors.append(
|
|
402
|
+
ValidationError(
|
|
403
|
+
f"Number above maximum",
|
|
404
|
+
path=path,
|
|
405
|
+
value=value,
|
|
406
|
+
expected=f"maximum: {schema['maximum']}",
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if "exclusiveMinimum" in schema and value <= schema["exclusiveMinimum"]:
|
|
411
|
+
errors.append(
|
|
412
|
+
ValidationError(
|
|
413
|
+
f"Number not greater than exclusive minimum",
|
|
414
|
+
path=path,
|
|
415
|
+
value=value,
|
|
416
|
+
expected=f"exclusiveMinimum: {schema['exclusiveMinimum']}",
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if "exclusiveMaximum" in schema and value >= schema["exclusiveMaximum"]:
|
|
421
|
+
errors.append(
|
|
422
|
+
ValidationError(
|
|
423
|
+
f"Number not less than exclusive maximum",
|
|
424
|
+
path=path,
|
|
425
|
+
value=value,
|
|
426
|
+
expected=f"exclusiveMaximum: {schema['exclusiveMaximum']}",
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if "multipleOf" in schema and value % schema["multipleOf"] != 0:
|
|
431
|
+
errors.append(
|
|
432
|
+
ValidationError(
|
|
433
|
+
f"Number is not a multiple",
|
|
434
|
+
path=path,
|
|
435
|
+
value=value,
|
|
436
|
+
expected=f"multipleOf: {schema['multipleOf']}",
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def _validate_array(
|
|
441
|
+
self,
|
|
442
|
+
value: List[Any],
|
|
443
|
+
schema: Dict[str, Any],
|
|
444
|
+
path: str,
|
|
445
|
+
errors: List[ValidationError],
|
|
446
|
+
warnings: List[str],
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Validate array-specific constraints."""
|
|
449
|
+
if "minItems" in schema and len(value) < schema["minItems"]:
|
|
450
|
+
errors.append(
|
|
451
|
+
ValidationError(
|
|
452
|
+
f"Array too short (length: {len(value)})",
|
|
453
|
+
path=path,
|
|
454
|
+
value=f"[{len(value)} items]",
|
|
455
|
+
expected=f"minItems: {schema['minItems']}",
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if "maxItems" in schema and len(value) > schema["maxItems"]:
|
|
460
|
+
errors.append(
|
|
461
|
+
ValidationError(
|
|
462
|
+
f"Array too long (length: {len(value)})",
|
|
463
|
+
path=path,
|
|
464
|
+
value=f"[{len(value)} items]",
|
|
465
|
+
expected=f"maxItems: {schema['maxItems']}",
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if "uniqueItems" in schema and schema["uniqueItems"]:
|
|
470
|
+
try:
|
|
471
|
+
# Check for duplicates using JSON serialization for hashability
|
|
472
|
+
seen = set()
|
|
473
|
+
for item in value:
|
|
474
|
+
key = json.dumps(item, sort_keys=True, default=str)
|
|
475
|
+
if key in seen:
|
|
476
|
+
errors.append(
|
|
477
|
+
ValidationError(
|
|
478
|
+
f"Array contains duplicate items",
|
|
479
|
+
path=path,
|
|
480
|
+
value=item,
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
break
|
|
484
|
+
seen.add(key)
|
|
485
|
+
except Exception:
|
|
486
|
+
warnings.append(f"Could not check uniqueItems at {path}")
|
|
487
|
+
|
|
488
|
+
# Validate items
|
|
489
|
+
if "items" in schema:
|
|
490
|
+
item_schema = schema["items"]
|
|
491
|
+
for i, item in enumerate(value):
|
|
492
|
+
item_path = f"{path}[{i}]"
|
|
493
|
+
self._validate_value(item, item_schema, item_path, errors, warnings)
|
|
494
|
+
|
|
495
|
+
def _validate_object(
|
|
496
|
+
self,
|
|
497
|
+
value: Dict[str, Any],
|
|
498
|
+
schema: Dict[str, Any],
|
|
499
|
+
path: str,
|
|
500
|
+
errors: List[ValidationError],
|
|
501
|
+
warnings: List[str],
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Validate object-specific constraints."""
|
|
504
|
+
# Check required properties
|
|
505
|
+
required = schema.get("required", [])
|
|
506
|
+
for prop in required:
|
|
507
|
+
if prop not in value:
|
|
508
|
+
errors.append(
|
|
509
|
+
ValidationError(
|
|
510
|
+
f"Missing required property: {prop}",
|
|
511
|
+
path=path or "$",
|
|
512
|
+
expected=f"property '{prop}'",
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Validate properties
|
|
517
|
+
properties = schema.get("properties", {})
|
|
518
|
+
additional_properties = schema.get(
|
|
519
|
+
"additionalProperties", not self.strict
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
for key, prop_value in value.items():
|
|
523
|
+
prop_path = f"{path}.{key}" if path else key
|
|
524
|
+
|
|
525
|
+
if key in properties:
|
|
526
|
+
self._validate_value(
|
|
527
|
+
prop_value, properties[key], prop_path, errors, warnings
|
|
528
|
+
)
|
|
529
|
+
elif not additional_properties:
|
|
530
|
+
errors.append(
|
|
531
|
+
ValidationError(
|
|
532
|
+
f"Additional property not allowed: {key}",
|
|
533
|
+
path=prop_path,
|
|
534
|
+
value=prop_value,
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
elif isinstance(additional_properties, dict):
|
|
538
|
+
# additionalProperties can be a schema
|
|
539
|
+
self._validate_value(
|
|
540
|
+
prop_value, additional_properties, prop_path, errors, warnings
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Check property count
|
|
544
|
+
if "minProperties" in schema and len(value) < schema["minProperties"]:
|
|
545
|
+
errors.append(
|
|
546
|
+
ValidationError(
|
|
547
|
+
f"Object has too few properties",
|
|
548
|
+
path=path or "$",
|
|
549
|
+
value=f"{len(value)} properties",
|
|
550
|
+
expected=f"minProperties: {schema['minProperties']}",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if "maxProperties" in schema and len(value) > schema["maxProperties"]:
|
|
555
|
+
errors.append(
|
|
556
|
+
ValidationError(
|
|
557
|
+
f"Object has too many properties",
|
|
558
|
+
path=path or "$",
|
|
559
|
+
value=f"{len(value)} properties",
|
|
560
|
+
expected=f"maxProperties: {schema['maxProperties']}",
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
565
|
+
"""Convert to dictionary representation."""
|
|
566
|
+
return {
|
|
567
|
+
"type": "json_schema",
|
|
568
|
+
"name": self.name,
|
|
569
|
+
"strict": self.strict,
|
|
570
|
+
"schema": self.schema,
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class XMLSchema(ReportSchema):
|
|
575
|
+
"""XML structure validator for reporter output.
|
|
576
|
+
|
|
577
|
+
Validates XML output structure including elements, attributes,
|
|
578
|
+
and text content.
|
|
579
|
+
|
|
580
|
+
Example:
|
|
581
|
+
>>> schema = XMLSchema(
|
|
582
|
+
... root_element="report",
|
|
583
|
+
... required_elements=["summary", "results"],
|
|
584
|
+
... required_attributes={"report": ["version"]},
|
|
585
|
+
... )
|
|
586
|
+
>>> result = schema.validate(xml_string)
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
def __init__(
|
|
590
|
+
self,
|
|
591
|
+
root_element: str,
|
|
592
|
+
required_elements: Optional[List[str]] = None,
|
|
593
|
+
required_attributes: Optional[Dict[str, List[str]]] = None,
|
|
594
|
+
element_schemas: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
595
|
+
name: Optional[str] = None,
|
|
596
|
+
) -> None:
|
|
597
|
+
"""Initialize XML Schema validator.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
root_element: Expected root element name.
|
|
601
|
+
required_elements: List of required element names.
|
|
602
|
+
required_attributes: Dict mapping element names to required attributes.
|
|
603
|
+
element_schemas: Dict mapping element names to validation rules.
|
|
604
|
+
name: Optional schema name.
|
|
605
|
+
"""
|
|
606
|
+
super().__init__(name)
|
|
607
|
+
self.root_element = root_element
|
|
608
|
+
self.required_elements = required_elements or []
|
|
609
|
+
self.required_attributes = required_attributes or {}
|
|
610
|
+
self.element_schemas = element_schemas or {}
|
|
611
|
+
|
|
612
|
+
def validate(self, output: Union[str, bytes, ElementTree.Element]) -> ValidationResult:
|
|
613
|
+
"""Validate XML output."""
|
|
614
|
+
from datetime import datetime
|
|
615
|
+
|
|
616
|
+
errors: List[ValidationError] = []
|
|
617
|
+
warnings: List[str] = []
|
|
618
|
+
|
|
619
|
+
# Parse XML if string/bytes
|
|
620
|
+
try:
|
|
621
|
+
if isinstance(output, (str, bytes)):
|
|
622
|
+
root = ElementTree.fromstring(output)
|
|
623
|
+
elif isinstance(output, ElementTree.Element):
|
|
624
|
+
root = output
|
|
625
|
+
else:
|
|
626
|
+
errors.append(
|
|
627
|
+
ValidationError(
|
|
628
|
+
f"Expected XML string, bytes, or Element, got {type(output).__name__}",
|
|
629
|
+
path="$",
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
return ValidationResult(
|
|
633
|
+
valid=False,
|
|
634
|
+
errors=errors,
|
|
635
|
+
schema_name=self.name,
|
|
636
|
+
checked_at=datetime.now().isoformat(),
|
|
637
|
+
)
|
|
638
|
+
except ElementTree.ParseError as e:
|
|
639
|
+
errors.append(
|
|
640
|
+
ValidationError(
|
|
641
|
+
f"XML parse error: {e}",
|
|
642
|
+
path="$",
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
return ValidationResult(
|
|
646
|
+
valid=False,
|
|
647
|
+
errors=errors,
|
|
648
|
+
schema_name=self.name,
|
|
649
|
+
checked_at=datetime.now().isoformat(),
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Validate root element
|
|
653
|
+
if root.tag != self.root_element:
|
|
654
|
+
errors.append(
|
|
655
|
+
ValidationError(
|
|
656
|
+
f"Invalid root element",
|
|
657
|
+
path="$",
|
|
658
|
+
value=root.tag,
|
|
659
|
+
expected=self.root_element,
|
|
660
|
+
)
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Validate required elements
|
|
664
|
+
found_elements = {elem.tag for elem in root.iter()}
|
|
665
|
+
for required in self.required_elements:
|
|
666
|
+
if required not in found_elements:
|
|
667
|
+
errors.append(
|
|
668
|
+
ValidationError(
|
|
669
|
+
f"Missing required element: {required}",
|
|
670
|
+
path="$",
|
|
671
|
+
expected=required,
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# Validate required attributes
|
|
676
|
+
for elem_name, attrs in self.required_attributes.items():
|
|
677
|
+
for elem in root.iter(elem_name):
|
|
678
|
+
for attr in attrs:
|
|
679
|
+
if attr not in elem.attrib:
|
|
680
|
+
errors.append(
|
|
681
|
+
ValidationError(
|
|
682
|
+
f"Missing required attribute: {attr}",
|
|
683
|
+
path=elem_name,
|
|
684
|
+
expected=attr,
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# Validate element schemas
|
|
689
|
+
for elem_name, schema in self.element_schemas.items():
|
|
690
|
+
for elem in root.iter(elem_name):
|
|
691
|
+
self._validate_element(elem, schema, elem_name, errors)
|
|
692
|
+
|
|
693
|
+
return ValidationResult(
|
|
694
|
+
valid=len(errors) == 0,
|
|
695
|
+
errors=errors,
|
|
696
|
+
warnings=warnings,
|
|
697
|
+
schema_name=self.name,
|
|
698
|
+
checked_at=datetime.now().isoformat(),
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def _validate_element(
|
|
702
|
+
self,
|
|
703
|
+
element: ElementTree.Element,
|
|
704
|
+
schema: Dict[str, Any],
|
|
705
|
+
path: str,
|
|
706
|
+
errors: List[ValidationError],
|
|
707
|
+
) -> None:
|
|
708
|
+
"""Validate element against schema."""
|
|
709
|
+
# Validate text content
|
|
710
|
+
if "text" in schema:
|
|
711
|
+
text_schema = schema["text"]
|
|
712
|
+
text = element.text or ""
|
|
713
|
+
|
|
714
|
+
if "pattern" in text_schema:
|
|
715
|
+
if not re.match(text_schema["pattern"], text):
|
|
716
|
+
errors.append(
|
|
717
|
+
ValidationError(
|
|
718
|
+
f"Element text does not match pattern",
|
|
719
|
+
path=path,
|
|
720
|
+
value=text,
|
|
721
|
+
expected=f"pattern: {text_schema['pattern']}",
|
|
722
|
+
)
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
if "minLength" in text_schema and len(text) < text_schema["minLength"]:
|
|
726
|
+
errors.append(
|
|
727
|
+
ValidationError(
|
|
728
|
+
f"Element text too short",
|
|
729
|
+
path=path,
|
|
730
|
+
value=text,
|
|
731
|
+
expected=f"minLength: {text_schema['minLength']}",
|
|
732
|
+
)
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
# Validate child count
|
|
736
|
+
if "minChildren" in schema:
|
|
737
|
+
if len(element) < schema["minChildren"]:
|
|
738
|
+
errors.append(
|
|
739
|
+
ValidationError(
|
|
740
|
+
f"Element has too few children",
|
|
741
|
+
path=path,
|
|
742
|
+
value=f"{len(element)} children",
|
|
743
|
+
expected=f"minChildren: {schema['minChildren']}",
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
if "maxChildren" in schema:
|
|
748
|
+
if len(element) > schema["maxChildren"]:
|
|
749
|
+
errors.append(
|
|
750
|
+
ValidationError(
|
|
751
|
+
f"Element has too many children",
|
|
752
|
+
path=path,
|
|
753
|
+
value=f"{len(element)} children",
|
|
754
|
+
expected=f"maxChildren: {schema['maxChildren']}",
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
759
|
+
"""Convert to dictionary representation."""
|
|
760
|
+
return {
|
|
761
|
+
"type": "xml_schema",
|
|
762
|
+
"name": self.name,
|
|
763
|
+
"root_element": self.root_element,
|
|
764
|
+
"required_elements": self.required_elements,
|
|
765
|
+
"required_attributes": self.required_attributes,
|
|
766
|
+
"element_schemas": self.element_schemas,
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
class CSVSchema(ReportSchema):
|
|
771
|
+
"""CSV structure validator for reporter output.
|
|
772
|
+
|
|
773
|
+
Validates CSV output including headers, column count,
|
|
774
|
+
and column value constraints.
|
|
775
|
+
|
|
776
|
+
Example:
|
|
777
|
+
>>> schema = CSVSchema(
|
|
778
|
+
... required_columns=["id", "name", "status"],
|
|
779
|
+
... column_types={"id": "integer", "status": "enum:pass,fail"},
|
|
780
|
+
... delimiter=",",
|
|
781
|
+
... )
|
|
782
|
+
>>> result = schema.validate(csv_string)
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
def __init__(
|
|
786
|
+
self,
|
|
787
|
+
required_columns: Optional[List[str]] = None,
|
|
788
|
+
column_types: Optional[Dict[str, str]] = None,
|
|
789
|
+
delimiter: str = ",",
|
|
790
|
+
has_header: bool = True,
|
|
791
|
+
min_rows: Optional[int] = None,
|
|
792
|
+
max_rows: Optional[int] = None,
|
|
793
|
+
name: Optional[str] = None,
|
|
794
|
+
) -> None:
|
|
795
|
+
"""Initialize CSV Schema validator.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
required_columns: List of required column names.
|
|
799
|
+
column_types: Dict mapping column names to type constraints.
|
|
800
|
+
delimiter: CSV delimiter character.
|
|
801
|
+
has_header: Whether CSV has a header row.
|
|
802
|
+
min_rows: Minimum number of data rows.
|
|
803
|
+
max_rows: Maximum number of data rows.
|
|
804
|
+
name: Optional schema name.
|
|
805
|
+
"""
|
|
806
|
+
super().__init__(name)
|
|
807
|
+
self.required_columns = required_columns or []
|
|
808
|
+
self.column_types = column_types or {}
|
|
809
|
+
self.delimiter = delimiter
|
|
810
|
+
self.has_header = has_header
|
|
811
|
+
self.min_rows = min_rows
|
|
812
|
+
self.max_rows = max_rows
|
|
813
|
+
|
|
814
|
+
def validate(self, output: str) -> ValidationResult:
|
|
815
|
+
"""Validate CSV output."""
|
|
816
|
+
import csv
|
|
817
|
+
from datetime import datetime
|
|
818
|
+
from io import StringIO
|
|
819
|
+
|
|
820
|
+
errors: List[ValidationError] = []
|
|
821
|
+
warnings: List[str] = []
|
|
822
|
+
|
|
823
|
+
if not isinstance(output, str):
|
|
824
|
+
errors.append(
|
|
825
|
+
ValidationError(
|
|
826
|
+
f"Expected string, got {type(output).__name__}",
|
|
827
|
+
path="$",
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
return ValidationResult(
|
|
831
|
+
valid=False,
|
|
832
|
+
errors=errors,
|
|
833
|
+
schema_name=self.name,
|
|
834
|
+
checked_at=datetime.now().isoformat(),
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
try:
|
|
838
|
+
reader = csv.reader(StringIO(output), delimiter=self.delimiter)
|
|
839
|
+
rows = list(reader)
|
|
840
|
+
except Exception as e:
|
|
841
|
+
errors.append(
|
|
842
|
+
ValidationError(
|
|
843
|
+
f"CSV parse error: {e}",
|
|
844
|
+
path="$",
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
return ValidationResult(
|
|
848
|
+
valid=False,
|
|
849
|
+
errors=errors,
|
|
850
|
+
schema_name=self.name,
|
|
851
|
+
checked_at=datetime.now().isoformat(),
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
if not rows:
|
|
855
|
+
if self.required_columns or self.min_rows:
|
|
856
|
+
errors.append(
|
|
857
|
+
ValidationError(
|
|
858
|
+
"CSV is empty",
|
|
859
|
+
path="$",
|
|
860
|
+
)
|
|
861
|
+
)
|
|
862
|
+
return ValidationResult(
|
|
863
|
+
valid=len(errors) == 0,
|
|
864
|
+
errors=errors,
|
|
865
|
+
schema_name=self.name,
|
|
866
|
+
checked_at=datetime.now().isoformat(),
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# Extract header and data
|
|
870
|
+
if self.has_header:
|
|
871
|
+
headers = rows[0]
|
|
872
|
+
data_rows = rows[1:]
|
|
873
|
+
else:
|
|
874
|
+
headers = []
|
|
875
|
+
data_rows = rows
|
|
876
|
+
|
|
877
|
+
# Validate required columns
|
|
878
|
+
if self.has_header:
|
|
879
|
+
for col in self.required_columns:
|
|
880
|
+
if col not in headers:
|
|
881
|
+
errors.append(
|
|
882
|
+
ValidationError(
|
|
883
|
+
f"Missing required column: {col}",
|
|
884
|
+
path="header",
|
|
885
|
+
expected=col,
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# Validate row count
|
|
890
|
+
if self.min_rows is not None and len(data_rows) < self.min_rows:
|
|
891
|
+
errors.append(
|
|
892
|
+
ValidationError(
|
|
893
|
+
f"Too few data rows",
|
|
894
|
+
path="$",
|
|
895
|
+
value=f"{len(data_rows)} rows",
|
|
896
|
+
expected=f"minRows: {self.min_rows}",
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
if self.max_rows is not None and len(data_rows) > self.max_rows:
|
|
901
|
+
errors.append(
|
|
902
|
+
ValidationError(
|
|
903
|
+
f"Too many data rows",
|
|
904
|
+
path="$",
|
|
905
|
+
value=f"{len(data_rows)} rows",
|
|
906
|
+
expected=f"maxRows: {self.max_rows}",
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
# Validate column types
|
|
911
|
+
if self.has_header and self.column_types:
|
|
912
|
+
for i, row in enumerate(data_rows):
|
|
913
|
+
for col_name, type_constraint in self.column_types.items():
|
|
914
|
+
if col_name in headers:
|
|
915
|
+
col_idx = headers.index(col_name)
|
|
916
|
+
if col_idx < len(row):
|
|
917
|
+
value = row[col_idx]
|
|
918
|
+
self._validate_column_type(
|
|
919
|
+
value, type_constraint, f"row[{i}].{col_name}", errors
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
return ValidationResult(
|
|
923
|
+
valid=len(errors) == 0,
|
|
924
|
+
errors=errors,
|
|
925
|
+
warnings=warnings,
|
|
926
|
+
schema_name=self.name,
|
|
927
|
+
checked_at=datetime.now().isoformat(),
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
def _validate_column_type(
|
|
931
|
+
self,
|
|
932
|
+
value: str,
|
|
933
|
+
type_constraint: str,
|
|
934
|
+
path: str,
|
|
935
|
+
errors: List[ValidationError],
|
|
936
|
+
) -> None:
|
|
937
|
+
"""Validate column value against type constraint."""
|
|
938
|
+
if type_constraint == "integer":
|
|
939
|
+
try:
|
|
940
|
+
int(value)
|
|
941
|
+
except ValueError:
|
|
942
|
+
errors.append(
|
|
943
|
+
ValidationError(
|
|
944
|
+
f"Value is not an integer",
|
|
945
|
+
path=path,
|
|
946
|
+
value=value,
|
|
947
|
+
expected="integer",
|
|
948
|
+
)
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
elif type_constraint == "number":
|
|
952
|
+
try:
|
|
953
|
+
float(value)
|
|
954
|
+
except ValueError:
|
|
955
|
+
errors.append(
|
|
956
|
+
ValidationError(
|
|
957
|
+
f"Value is not a number",
|
|
958
|
+
path=path,
|
|
959
|
+
value=value,
|
|
960
|
+
expected="number",
|
|
961
|
+
)
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
elif type_constraint == "boolean":
|
|
965
|
+
if value.lower() not in ("true", "false", "1", "0", "yes", "no"):
|
|
966
|
+
errors.append(
|
|
967
|
+
ValidationError(
|
|
968
|
+
f"Value is not a boolean",
|
|
969
|
+
path=path,
|
|
970
|
+
value=value,
|
|
971
|
+
expected="boolean",
|
|
972
|
+
)
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
elif type_constraint.startswith("enum:"):
|
|
976
|
+
allowed = type_constraint[5:].split(",")
|
|
977
|
+
if value not in allowed:
|
|
978
|
+
errors.append(
|
|
979
|
+
ValidationError(
|
|
980
|
+
f"Value not in enum",
|
|
981
|
+
path=path,
|
|
982
|
+
value=value,
|
|
983
|
+
expected=f"one of: {allowed}",
|
|
984
|
+
)
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
elif type_constraint.startswith("pattern:"):
|
|
988
|
+
pattern = type_constraint[8:]
|
|
989
|
+
if not re.match(pattern, value):
|
|
990
|
+
errors.append(
|
|
991
|
+
ValidationError(
|
|
992
|
+
f"Value does not match pattern",
|
|
993
|
+
path=path,
|
|
994
|
+
value=value,
|
|
995
|
+
expected=f"pattern: {pattern}",
|
|
996
|
+
)
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1000
|
+
"""Convert to dictionary representation."""
|
|
1001
|
+
return {
|
|
1002
|
+
"type": "csv_schema",
|
|
1003
|
+
"name": self.name,
|
|
1004
|
+
"required_columns": self.required_columns,
|
|
1005
|
+
"column_types": self.column_types,
|
|
1006
|
+
"delimiter": self.delimiter,
|
|
1007
|
+
"has_header": self.has_header,
|
|
1008
|
+
"min_rows": self.min_rows,
|
|
1009
|
+
"max_rows": self.max_rows,
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
class TextSchema(ReportSchema):
|
|
1014
|
+
"""Text/plain format validator for reporter output.
|
|
1015
|
+
|
|
1016
|
+
Validates text output using patterns, line constraints,
|
|
1017
|
+
and content requirements.
|
|
1018
|
+
|
|
1019
|
+
Example:
|
|
1020
|
+
>>> schema = TextSchema(
|
|
1021
|
+
... required_patterns=[r"Summary:", r"Total: \d+"],
|
|
1022
|
+
... forbidden_patterns=[r"ERROR:", r"FATAL:"],
|
|
1023
|
+
... min_lines=5,
|
|
1024
|
+
... )
|
|
1025
|
+
>>> result = schema.validate(text_output)
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
def __init__(
|
|
1029
|
+
self,
|
|
1030
|
+
required_patterns: Optional[List[str]] = None,
|
|
1031
|
+
forbidden_patterns: Optional[List[str]] = None,
|
|
1032
|
+
min_lines: Optional[int] = None,
|
|
1033
|
+
max_lines: Optional[int] = None,
|
|
1034
|
+
min_length: Optional[int] = None,
|
|
1035
|
+
max_length: Optional[int] = None,
|
|
1036
|
+
encoding: str = "utf-8",
|
|
1037
|
+
name: Optional[str] = None,
|
|
1038
|
+
) -> None:
|
|
1039
|
+
"""Initialize Text Schema validator.
|
|
1040
|
+
|
|
1041
|
+
Args:
|
|
1042
|
+
required_patterns: Regex patterns that must appear in output.
|
|
1043
|
+
forbidden_patterns: Regex patterns that must not appear.
|
|
1044
|
+
min_lines: Minimum number of lines.
|
|
1045
|
+
max_lines: Maximum number of lines.
|
|
1046
|
+
min_length: Minimum total character length.
|
|
1047
|
+
max_length: Maximum total character length.
|
|
1048
|
+
encoding: Expected text encoding.
|
|
1049
|
+
name: Optional schema name.
|
|
1050
|
+
"""
|
|
1051
|
+
super().__init__(name)
|
|
1052
|
+
self.required_patterns = required_patterns or []
|
|
1053
|
+
self.forbidden_patterns = forbidden_patterns or []
|
|
1054
|
+
self.min_lines = min_lines
|
|
1055
|
+
self.max_lines = max_lines
|
|
1056
|
+
self.min_length = min_length
|
|
1057
|
+
self.max_length = max_length
|
|
1058
|
+
self.encoding = encoding
|
|
1059
|
+
|
|
1060
|
+
def validate(self, output: Union[str, bytes]) -> ValidationResult:
|
|
1061
|
+
"""Validate text output."""
|
|
1062
|
+
from datetime import datetime
|
|
1063
|
+
|
|
1064
|
+
errors: List[ValidationError] = []
|
|
1065
|
+
warnings: List[str] = []
|
|
1066
|
+
|
|
1067
|
+
# Convert bytes to string
|
|
1068
|
+
if isinstance(output, bytes):
|
|
1069
|
+
try:
|
|
1070
|
+
output = output.decode(self.encoding)
|
|
1071
|
+
except UnicodeDecodeError as e:
|
|
1072
|
+
errors.append(
|
|
1073
|
+
ValidationError(
|
|
1074
|
+
f"Encoding error: {e}",
|
|
1075
|
+
path="$",
|
|
1076
|
+
expected=f"encoding: {self.encoding}",
|
|
1077
|
+
)
|
|
1078
|
+
)
|
|
1079
|
+
return ValidationResult(
|
|
1080
|
+
valid=False,
|
|
1081
|
+
errors=errors,
|
|
1082
|
+
schema_name=self.name,
|
|
1083
|
+
checked_at=datetime.now().isoformat(),
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
if not isinstance(output, str):
|
|
1087
|
+
errors.append(
|
|
1088
|
+
ValidationError(
|
|
1089
|
+
f"Expected string or bytes, got {type(output).__name__}",
|
|
1090
|
+
path="$",
|
|
1091
|
+
)
|
|
1092
|
+
)
|
|
1093
|
+
return ValidationResult(
|
|
1094
|
+
valid=False,
|
|
1095
|
+
errors=errors,
|
|
1096
|
+
schema_name=self.name,
|
|
1097
|
+
checked_at=datetime.now().isoformat(),
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# Validate length
|
|
1101
|
+
if self.min_length is not None and len(output) < self.min_length:
|
|
1102
|
+
errors.append(
|
|
1103
|
+
ValidationError(
|
|
1104
|
+
f"Text too short",
|
|
1105
|
+
path="$",
|
|
1106
|
+
value=f"{len(output)} chars",
|
|
1107
|
+
expected=f"minLength: {self.min_length}",
|
|
1108
|
+
)
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
if self.max_length is not None and len(output) > self.max_length:
|
|
1112
|
+
errors.append(
|
|
1113
|
+
ValidationError(
|
|
1114
|
+
f"Text too long",
|
|
1115
|
+
path="$",
|
|
1116
|
+
value=f"{len(output)} chars",
|
|
1117
|
+
expected=f"maxLength: {self.max_length}",
|
|
1118
|
+
)
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# Validate line count
|
|
1122
|
+
lines = output.splitlines()
|
|
1123
|
+
if self.min_lines is not None and len(lines) < self.min_lines:
|
|
1124
|
+
errors.append(
|
|
1125
|
+
ValidationError(
|
|
1126
|
+
f"Too few lines",
|
|
1127
|
+
path="$",
|
|
1128
|
+
value=f"{len(lines)} lines",
|
|
1129
|
+
expected=f"minLines: {self.min_lines}",
|
|
1130
|
+
)
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
if self.max_lines is not None and len(lines) > self.max_lines:
|
|
1134
|
+
errors.append(
|
|
1135
|
+
ValidationError(
|
|
1136
|
+
f"Too many lines",
|
|
1137
|
+
path="$",
|
|
1138
|
+
value=f"{len(lines)} lines",
|
|
1139
|
+
expected=f"maxLines: {self.max_lines}",
|
|
1140
|
+
)
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
# Validate required patterns
|
|
1144
|
+
for pattern in self.required_patterns:
|
|
1145
|
+
if not re.search(pattern, output):
|
|
1146
|
+
errors.append(
|
|
1147
|
+
ValidationError(
|
|
1148
|
+
f"Required pattern not found",
|
|
1149
|
+
path="$",
|
|
1150
|
+
expected=f"pattern: {pattern}",
|
|
1151
|
+
)
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
# Validate forbidden patterns
|
|
1155
|
+
for pattern in self.forbidden_patterns:
|
|
1156
|
+
match = re.search(pattern, output)
|
|
1157
|
+
if match:
|
|
1158
|
+
errors.append(
|
|
1159
|
+
ValidationError(
|
|
1160
|
+
f"Forbidden pattern found",
|
|
1161
|
+
path="$",
|
|
1162
|
+
value=match.group(),
|
|
1163
|
+
expected=f"not: {pattern}",
|
|
1164
|
+
)
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
return ValidationResult(
|
|
1168
|
+
valid=len(errors) == 0,
|
|
1169
|
+
errors=errors,
|
|
1170
|
+
warnings=warnings,
|
|
1171
|
+
schema_name=self.name,
|
|
1172
|
+
checked_at=datetime.now().isoformat(),
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1176
|
+
"""Convert to dictionary representation."""
|
|
1177
|
+
return {
|
|
1178
|
+
"type": "text_schema",
|
|
1179
|
+
"name": self.name,
|
|
1180
|
+
"required_patterns": self.required_patterns,
|
|
1181
|
+
"forbidden_patterns": self.forbidden_patterns,
|
|
1182
|
+
"min_lines": self.min_lines,
|
|
1183
|
+
"max_lines": self.max_lines,
|
|
1184
|
+
"min_length": self.min_length,
|
|
1185
|
+
"max_length": self.max_length,
|
|
1186
|
+
"encoding": self.encoding,
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
# Schema registry
|
|
1191
|
+
_schema_registry: Dict[str, ReportSchema] = {}
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def register_schema(name: str, schema: ReportSchema) -> None:
|
|
1195
|
+
"""Register a schema for a reporter.
|
|
1196
|
+
|
|
1197
|
+
Args:
|
|
1198
|
+
name: Schema name (typically reporter name).
|
|
1199
|
+
schema: The schema to register.
|
|
1200
|
+
|
|
1201
|
+
Example:
|
|
1202
|
+
>>> schema = JSONSchema({...})
|
|
1203
|
+
>>> register_schema("my_reporter", schema)
|
|
1204
|
+
"""
|
|
1205
|
+
_schema_registry[name] = schema
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def get_schema(name: str) -> Optional[ReportSchema]:
|
|
1209
|
+
"""Get a registered schema by name.
|
|
1210
|
+
|
|
1211
|
+
Args:
|
|
1212
|
+
name: Schema name to look up.
|
|
1213
|
+
|
|
1214
|
+
Returns:
|
|
1215
|
+
The registered schema, or None if not found.
|
|
1216
|
+
"""
|
|
1217
|
+
return _schema_registry.get(name)
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def unregister_schema(name: str) -> bool:
|
|
1221
|
+
"""Unregister a schema.
|
|
1222
|
+
|
|
1223
|
+
Args:
|
|
1224
|
+
name: Schema name to unregister.
|
|
1225
|
+
|
|
1226
|
+
Returns:
|
|
1227
|
+
True if schema was removed, False if not found.
|
|
1228
|
+
"""
|
|
1229
|
+
if name in _schema_registry:
|
|
1230
|
+
del _schema_registry[name]
|
|
1231
|
+
return True
|
|
1232
|
+
return False
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def validate_output(
|
|
1236
|
+
output: Any,
|
|
1237
|
+
schema: Optional[ReportSchema] = None,
|
|
1238
|
+
schema_name: Optional[str] = None,
|
|
1239
|
+
) -> ValidationResult:
|
|
1240
|
+
"""Validate output against a schema.
|
|
1241
|
+
|
|
1242
|
+
Args:
|
|
1243
|
+
output: The output to validate.
|
|
1244
|
+
schema: Schema to validate against.
|
|
1245
|
+
schema_name: Name of a registered schema to use.
|
|
1246
|
+
|
|
1247
|
+
Returns:
|
|
1248
|
+
ValidationResult with validation status.
|
|
1249
|
+
|
|
1250
|
+
Raises:
|
|
1251
|
+
ValueError: If neither schema nor schema_name provided.
|
|
1252
|
+
"""
|
|
1253
|
+
if schema is None and schema_name is not None:
|
|
1254
|
+
schema = get_schema(schema_name)
|
|
1255
|
+
if schema is None:
|
|
1256
|
+
raise ValueError(f"No schema registered with name: {schema_name}")
|
|
1257
|
+
|
|
1258
|
+
if schema is None:
|
|
1259
|
+
raise ValueError("Must provide either schema or schema_name")
|
|
1260
|
+
|
|
1261
|
+
return schema.validate(output)
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def validate_reporter_output(
|
|
1265
|
+
schema: Optional[ReportSchema] = None,
|
|
1266
|
+
schema_name: Optional[str] = None,
|
|
1267
|
+
raise_on_error: bool = False,
|
|
1268
|
+
) -> Callable:
|
|
1269
|
+
"""Decorator to validate reporter output.
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
schema: Schema to validate against.
|
|
1273
|
+
schema_name: Name of registered schema to use.
|
|
1274
|
+
raise_on_error: If True, raise SchemaError on validation failure.
|
|
1275
|
+
|
|
1276
|
+
Returns:
|
|
1277
|
+
Decorator function.
|
|
1278
|
+
|
|
1279
|
+
Example:
|
|
1280
|
+
>>> @validate_reporter_output(schema=my_schema, raise_on_error=True)
|
|
1281
|
+
... def render(self, results):
|
|
1282
|
+
... return {"summary": ..., "results": ...}
|
|
1283
|
+
"""
|
|
1284
|
+
|
|
1285
|
+
def decorator(func: Callable) -> Callable:
|
|
1286
|
+
def wrapper(*args, **kwargs):
|
|
1287
|
+
output = func(*args, **kwargs)
|
|
1288
|
+
|
|
1289
|
+
result = validate_output(output, schema=schema, schema_name=schema_name)
|
|
1290
|
+
|
|
1291
|
+
if raise_on_error:
|
|
1292
|
+
result.raise_if_invalid()
|
|
1293
|
+
|
|
1294
|
+
return output
|
|
1295
|
+
|
|
1296
|
+
return wrapper
|
|
1297
|
+
|
|
1298
|
+
return decorator
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def infer_schema(output: Any) -> ReportSchema:
|
|
1302
|
+
"""Infer a schema from sample output.
|
|
1303
|
+
|
|
1304
|
+
This function attempts to generate a schema based on
|
|
1305
|
+
the structure of the provided output.
|
|
1306
|
+
|
|
1307
|
+
Args:
|
|
1308
|
+
output: Sample output to infer schema from.
|
|
1309
|
+
|
|
1310
|
+
Returns:
|
|
1311
|
+
Inferred ReportSchema.
|
|
1312
|
+
|
|
1313
|
+
Example:
|
|
1314
|
+
>>> sample = {"name": "test", "count": 5, "items": [1, 2, 3]}
|
|
1315
|
+
>>> schema = infer_schema(sample)
|
|
1316
|
+
>>> result = schema.validate(new_output)
|
|
1317
|
+
"""
|
|
1318
|
+
if isinstance(output, dict):
|
|
1319
|
+
return _infer_json_schema(output)
|
|
1320
|
+
elif isinstance(output, str):
|
|
1321
|
+
if output.strip().startswith("<"):
|
|
1322
|
+
return _infer_xml_schema(output)
|
|
1323
|
+
elif "," in output.split("\n")[0]:
|
|
1324
|
+
return _infer_csv_schema(output)
|
|
1325
|
+
else:
|
|
1326
|
+
return _infer_text_schema(output)
|
|
1327
|
+
else:
|
|
1328
|
+
# Default to JSON schema for other types
|
|
1329
|
+
return JSONSchema({"type": _python_to_json_type(type(output))})
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def _python_to_json_type(python_type: type) -> str:
|
|
1333
|
+
"""Convert Python type to JSON Schema type."""
|
|
1334
|
+
type_map = {
|
|
1335
|
+
str: "string",
|
|
1336
|
+
int: "integer",
|
|
1337
|
+
float: "number",
|
|
1338
|
+
bool: "boolean",
|
|
1339
|
+
list: "array",
|
|
1340
|
+
dict: "object",
|
|
1341
|
+
type(None): "null",
|
|
1342
|
+
}
|
|
1343
|
+
return type_map.get(python_type, "string")
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _infer_json_schema(obj: Dict[str, Any]) -> JSONSchema:
|
|
1347
|
+
"""Infer JSON Schema from dictionary."""
|
|
1348
|
+
|
|
1349
|
+
def infer_property_schema(value: Any) -> Dict[str, Any]:
|
|
1350
|
+
if value is None:
|
|
1351
|
+
return {"type": "null"}
|
|
1352
|
+
elif isinstance(value, bool):
|
|
1353
|
+
return {"type": "boolean"}
|
|
1354
|
+
elif isinstance(value, int):
|
|
1355
|
+
return {"type": "integer"}
|
|
1356
|
+
elif isinstance(value, float):
|
|
1357
|
+
return {"type": "number"}
|
|
1358
|
+
elif isinstance(value, str):
|
|
1359
|
+
return {"type": "string"}
|
|
1360
|
+
elif isinstance(value, list):
|
|
1361
|
+
if value:
|
|
1362
|
+
item_schema = infer_property_schema(value[0])
|
|
1363
|
+
return {"type": "array", "items": item_schema}
|
|
1364
|
+
return {"type": "array"}
|
|
1365
|
+
elif isinstance(value, dict):
|
|
1366
|
+
properties = {}
|
|
1367
|
+
for k, v in value.items():
|
|
1368
|
+
properties[k] = infer_property_schema(v)
|
|
1369
|
+
return {"type": "object", "properties": properties}
|
|
1370
|
+
return {}
|
|
1371
|
+
|
|
1372
|
+
properties = {}
|
|
1373
|
+
required = []
|
|
1374
|
+
|
|
1375
|
+
for key, value in obj.items():
|
|
1376
|
+
properties[key] = infer_property_schema(value)
|
|
1377
|
+
if value is not None:
|
|
1378
|
+
required.append(key)
|
|
1379
|
+
|
|
1380
|
+
return JSONSchema(
|
|
1381
|
+
{
|
|
1382
|
+
"type": "object",
|
|
1383
|
+
"properties": properties,
|
|
1384
|
+
"required": required,
|
|
1385
|
+
},
|
|
1386
|
+
name="inferred",
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def _infer_xml_schema(xml_str: str) -> XMLSchema:
|
|
1391
|
+
"""Infer XML Schema from XML string."""
|
|
1392
|
+
try:
|
|
1393
|
+
root = ElementTree.fromstring(xml_str)
|
|
1394
|
+
elements = {elem.tag for elem in root.iter()}
|
|
1395
|
+
elements.discard(root.tag)
|
|
1396
|
+
|
|
1397
|
+
return XMLSchema(
|
|
1398
|
+
root_element=root.tag,
|
|
1399
|
+
required_elements=list(elements),
|
|
1400
|
+
name="inferred",
|
|
1401
|
+
)
|
|
1402
|
+
except Exception:
|
|
1403
|
+
return XMLSchema(root_element="root", name="inferred")
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _infer_csv_schema(csv_str: str) -> CSVSchema:
|
|
1407
|
+
"""Infer CSV Schema from CSV string."""
|
|
1408
|
+
import csv
|
|
1409
|
+
from io import StringIO
|
|
1410
|
+
|
|
1411
|
+
try:
|
|
1412
|
+
reader = csv.reader(StringIO(csv_str))
|
|
1413
|
+
rows = list(reader)
|
|
1414
|
+
|
|
1415
|
+
if rows:
|
|
1416
|
+
headers = rows[0]
|
|
1417
|
+
return CSVSchema(
|
|
1418
|
+
required_columns=headers,
|
|
1419
|
+
has_header=True,
|
|
1420
|
+
min_rows=max(0, len(rows) - 1),
|
|
1421
|
+
name="inferred",
|
|
1422
|
+
)
|
|
1423
|
+
except Exception:
|
|
1424
|
+
pass
|
|
1425
|
+
|
|
1426
|
+
return CSVSchema(name="inferred")
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _infer_text_schema(text: str) -> TextSchema:
|
|
1430
|
+
"""Infer Text Schema from text string."""
|
|
1431
|
+
lines = text.splitlines()
|
|
1432
|
+
|
|
1433
|
+
return TextSchema(
|
|
1434
|
+
min_lines=len(lines),
|
|
1435
|
+
min_length=len(text) // 2, # Allow some variation
|
|
1436
|
+
max_length=len(text) * 2,
|
|
1437
|
+
name="inferred",
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def merge_schemas(
|
|
1442
|
+
schemas: List[ReportSchema],
|
|
1443
|
+
name: Optional[str] = None,
|
|
1444
|
+
) -> ReportSchema:
|
|
1445
|
+
"""Merge multiple schemas into a composite schema.
|
|
1446
|
+
|
|
1447
|
+
The merged schema validates against all provided schemas.
|
|
1448
|
+
|
|
1449
|
+
Args:
|
|
1450
|
+
schemas: List of schemas to merge.
|
|
1451
|
+
name: Optional name for the merged schema.
|
|
1452
|
+
|
|
1453
|
+
Returns:
|
|
1454
|
+
A new schema that combines all validations.
|
|
1455
|
+
|
|
1456
|
+
Example:
|
|
1457
|
+
>>> schema1 = JSONSchema({"type": "object"})
|
|
1458
|
+
>>> schema2 = TextSchema(min_length=10)
|
|
1459
|
+
>>> merged = merge_schemas([schema1, schema2])
|
|
1460
|
+
"""
|
|
1461
|
+
|
|
1462
|
+
class CompositeSchema(ReportSchema):
|
|
1463
|
+
def __init__(self, schemas: List[ReportSchema], name: Optional[str]) -> None:
|
|
1464
|
+
super().__init__(name or "composite")
|
|
1465
|
+
self.schemas = schemas
|
|
1466
|
+
|
|
1467
|
+
def validate(self, output: Any) -> ValidationResult:
|
|
1468
|
+
from datetime import datetime
|
|
1469
|
+
|
|
1470
|
+
all_errors: List[ValidationError] = []
|
|
1471
|
+
all_warnings: List[str] = []
|
|
1472
|
+
|
|
1473
|
+
for schema in self.schemas:
|
|
1474
|
+
result = schema.validate(output)
|
|
1475
|
+
all_errors.extend(result.errors)
|
|
1476
|
+
all_warnings.extend(result.warnings)
|
|
1477
|
+
|
|
1478
|
+
return ValidationResult(
|
|
1479
|
+
valid=len(all_errors) == 0,
|
|
1480
|
+
errors=all_errors,
|
|
1481
|
+
warnings=all_warnings,
|
|
1482
|
+
schema_name=self.name,
|
|
1483
|
+
checked_at=datetime.now().isoformat(),
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1487
|
+
return {
|
|
1488
|
+
"type": "composite",
|
|
1489
|
+
"name": self.name,
|
|
1490
|
+
"schemas": [s.to_dict() for s in self.schemas],
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return CompositeSchema(schemas, name)
|