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,1499 @@
|
|
|
1
|
+
"""OpsGenie integration action.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive OpsGenie integration for checkpoint notifications
|
|
4
|
+
using the OpsGenie Alert API v2.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Full OpsGenie Alert API v2 support
|
|
8
|
+
- Multiple alert actions (create, close, acknowledge, add note)
|
|
9
|
+
- Flexible responder configuration (users, teams, schedules, escalations)
|
|
10
|
+
- Auto-priority mapping from validation severity
|
|
11
|
+
- Deduplication for alert grouping
|
|
12
|
+
- Tag and custom property support
|
|
13
|
+
- Region-aware API endpoints (US, EU)
|
|
14
|
+
- Extensible payload builder pattern
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from truthound.checkpoint.actions import OpsGenieAction
|
|
18
|
+
>>>
|
|
19
|
+
>>> action = OpsGenieAction(
|
|
20
|
+
... api_key="your-api-key",
|
|
21
|
+
... notify_on="failure",
|
|
22
|
+
... auto_priority=True,
|
|
23
|
+
... responders=[
|
|
24
|
+
... {"type": "team", "name": "data-quality-team"},
|
|
25
|
+
... {"type": "user", "username": "admin@example.com"},
|
|
26
|
+
... ],
|
|
27
|
+
... tags=["data-quality", "production"],
|
|
28
|
+
... )
|
|
29
|
+
>>> result = action.execute(checkpoint_result)
|
|
30
|
+
|
|
31
|
+
References:
|
|
32
|
+
- OpsGenie Alert API: https://docs.opsgenie.com/docs/alert-api
|
|
33
|
+
- OpsGenie Integrations: https://docs.opsgenie.com/docs/integration-types-to-use
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
from abc import ABC, abstractmethod
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from datetime import datetime
|
|
42
|
+
from enum import Enum
|
|
43
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
44
|
+
|
|
45
|
+
from truthound.checkpoint.actions.base import (
|
|
46
|
+
ActionConfig,
|
|
47
|
+
ActionResult,
|
|
48
|
+
ActionStatus,
|
|
49
|
+
BaseAction,
|
|
50
|
+
NotifyCondition,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from truthound.checkpoint.checkpoint import CheckpointResult
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# Enums and Constants
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OpsGenieRegion(str, Enum):
|
|
63
|
+
"""OpsGenie API regions."""
|
|
64
|
+
|
|
65
|
+
US = "us"
|
|
66
|
+
EU = "eu"
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
return self.value
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def api_url(self) -> str:
|
|
73
|
+
"""Get API URL for this region."""
|
|
74
|
+
if self == OpsGenieRegion.EU:
|
|
75
|
+
return "https://api.eu.opsgenie.com"
|
|
76
|
+
return "https://api.opsgenie.com"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AlertPriority(str, Enum):
|
|
80
|
+
"""OpsGenie alert priority levels.
|
|
81
|
+
|
|
82
|
+
P1 is the highest priority, P5 is the lowest.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
P1 = "P1" # Critical
|
|
86
|
+
P2 = "P2" # High
|
|
87
|
+
P3 = "P3" # Moderate
|
|
88
|
+
P4 = "P4" # Low
|
|
89
|
+
P5 = "P5" # Informational
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
return self.value
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_severity(cls, severity: str) -> "AlertPriority":
|
|
96
|
+
"""Map severity string to priority.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
severity: Severity level (critical, high, medium, low, info).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Corresponding AlertPriority.
|
|
103
|
+
"""
|
|
104
|
+
mapping = {
|
|
105
|
+
"critical": cls.P1,
|
|
106
|
+
"high": cls.P2,
|
|
107
|
+
"medium": cls.P3,
|
|
108
|
+
"moderate": cls.P3,
|
|
109
|
+
"low": cls.P4,
|
|
110
|
+
"info": cls.P5,
|
|
111
|
+
"informational": cls.P5,
|
|
112
|
+
}
|
|
113
|
+
return mapping.get(severity.lower(), cls.P3)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AlertAction(str, Enum):
|
|
117
|
+
"""OpsGenie alert actions."""
|
|
118
|
+
|
|
119
|
+
CREATE = "create"
|
|
120
|
+
CLOSE = "close"
|
|
121
|
+
ACKNOWLEDGE = "acknowledge"
|
|
122
|
+
UNACKNOWLEDGE = "unacknowledge"
|
|
123
|
+
ADD_NOTE = "add_note"
|
|
124
|
+
SNOOZE = "snooze"
|
|
125
|
+
ESCALATE = "escalate"
|
|
126
|
+
ASSIGN = "assign"
|
|
127
|
+
ADD_TAGS = "add_tags"
|
|
128
|
+
REMOVE_TAGS = "remove_tags"
|
|
129
|
+
ADD_DETAILS = "add_details"
|
|
130
|
+
REMOVE_DETAILS = "remove_details"
|
|
131
|
+
|
|
132
|
+
def __str__(self) -> str:
|
|
133
|
+
return self.value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ResponderType(str, Enum):
|
|
137
|
+
"""Types of OpsGenie responders."""
|
|
138
|
+
|
|
139
|
+
USER = "user"
|
|
140
|
+
TEAM = "team"
|
|
141
|
+
ESCALATION = "escalation"
|
|
142
|
+
SCHEDULE = "schedule"
|
|
143
|
+
|
|
144
|
+
def __str__(self) -> str:
|
|
145
|
+
return self.value
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# =============================================================================
|
|
149
|
+
# Responder Configuration
|
|
150
|
+
# =============================================================================
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class Responder:
|
|
155
|
+
"""OpsGenie responder configuration.
|
|
156
|
+
|
|
157
|
+
Represents a user, team, escalation, or schedule that should be notified.
|
|
158
|
+
|
|
159
|
+
Attributes:
|
|
160
|
+
type: Type of responder (user, team, escalation, schedule).
|
|
161
|
+
id: Responder ID (optional, use either id or name/username).
|
|
162
|
+
name: Team/escalation/schedule name (for non-user types).
|
|
163
|
+
username: User email/username (for user type).
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
type: ResponderType | str
|
|
167
|
+
id: str | None = None
|
|
168
|
+
name: str | None = None
|
|
169
|
+
username: str | None = None
|
|
170
|
+
|
|
171
|
+
def __post_init__(self) -> None:
|
|
172
|
+
"""Validate and normalize responder type."""
|
|
173
|
+
if isinstance(self.type, str):
|
|
174
|
+
self.type = ResponderType(self.type.lower())
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict[str, str]:
|
|
177
|
+
"""Convert to OpsGenie API format.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dictionary in OpsGenie responder format.
|
|
181
|
+
"""
|
|
182
|
+
result: dict[str, str] = {"type": str(self.type)}
|
|
183
|
+
|
|
184
|
+
if self.id:
|
|
185
|
+
result["id"] = self.id
|
|
186
|
+
elif self.type == ResponderType.USER and self.username:
|
|
187
|
+
result["username"] = self.username
|
|
188
|
+
elif self.name:
|
|
189
|
+
result["name"] = self.name
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def user(cls, username: str) -> "Responder":
|
|
195
|
+
"""Create a user responder.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
username: User email/username.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Responder instance for a user.
|
|
202
|
+
"""
|
|
203
|
+
return cls(type=ResponderType.USER, username=username)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def team(cls, name: str | None = None, id: str | None = None) -> "Responder":
|
|
207
|
+
"""Create a team responder.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
name: Team name.
|
|
211
|
+
id: Team ID.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Responder instance for a team.
|
|
215
|
+
"""
|
|
216
|
+
return cls(type=ResponderType.TEAM, name=name, id=id)
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def escalation(cls, name: str | None = None, id: str | None = None) -> "Responder":
|
|
220
|
+
"""Create an escalation responder.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
name: Escalation name.
|
|
224
|
+
id: Escalation ID.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Responder instance for an escalation.
|
|
228
|
+
"""
|
|
229
|
+
return cls(type=ResponderType.ESCALATION, name=name, id=id)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def schedule(cls, name: str | None = None, id: str | None = None) -> "Responder":
|
|
233
|
+
"""Create a schedule responder.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name: Schedule name.
|
|
237
|
+
id: Schedule ID.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Responder instance for a schedule.
|
|
241
|
+
"""
|
|
242
|
+
return cls(type=ResponderType.SCHEDULE, name=name, id=id)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# =============================================================================
|
|
246
|
+
# Alert Payload Builder (Builder Pattern)
|
|
247
|
+
# =============================================================================
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@runtime_checkable
|
|
251
|
+
class AlertPayloadComponent(Protocol):
|
|
252
|
+
"""Protocol for alert payload components."""
|
|
253
|
+
|
|
254
|
+
def apply(self, payload: dict[str, Any]) -> None:
|
|
255
|
+
"""Apply this component to the payload."""
|
|
256
|
+
...
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AlertPayloadBuilder:
|
|
260
|
+
"""Fluent builder for creating OpsGenie alert payloads.
|
|
261
|
+
|
|
262
|
+
This builder provides a type-safe, fluent API for constructing
|
|
263
|
+
OpsGenie alert payloads with proper validation.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> payload = (
|
|
267
|
+
... AlertPayloadBuilder()
|
|
268
|
+
... .set_message("Data quality check failed")
|
|
269
|
+
... .set_priority(AlertPriority.P2)
|
|
270
|
+
... .add_responder(Responder.team("data-team"))
|
|
271
|
+
... .add_tag("production")
|
|
272
|
+
... .set_details({"checkpoint": "daily_check"})
|
|
273
|
+
... .build()
|
|
274
|
+
... )
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(self) -> None:
|
|
278
|
+
"""Initialize the builder."""
|
|
279
|
+
self._message: str = ""
|
|
280
|
+
self._alias: str | None = None
|
|
281
|
+
self._description: str | None = None
|
|
282
|
+
self._responders: list[Responder] = []
|
|
283
|
+
self._visible_to: list[Responder] = []
|
|
284
|
+
self._actions: list[str] = []
|
|
285
|
+
self._tags: list[str] = []
|
|
286
|
+
self._details: dict[str, str] = {}
|
|
287
|
+
self._entity: str | None = None
|
|
288
|
+
self._source: str | None = None
|
|
289
|
+
self._priority: AlertPriority = AlertPriority.P3
|
|
290
|
+
self._user: str | None = None
|
|
291
|
+
self._note: str | None = None
|
|
292
|
+
|
|
293
|
+
def set_message(self, message: str) -> "AlertPayloadBuilder":
|
|
294
|
+
"""Set the alert message (required, max 130 chars).
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
message: Alert message.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Self for method chaining.
|
|
301
|
+
"""
|
|
302
|
+
self._message = message[:130]
|
|
303
|
+
return self
|
|
304
|
+
|
|
305
|
+
def set_alias(self, alias: str) -> "AlertPayloadBuilder":
|
|
306
|
+
"""Set the alert alias for deduplication (max 512 chars).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
alias: Unique alias for grouping alerts.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Self for method chaining.
|
|
313
|
+
"""
|
|
314
|
+
self._alias = alias[:512]
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
def set_description(self, description: str) -> "AlertPayloadBuilder":
|
|
318
|
+
"""Set the alert description (max 15000 chars).
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
description: Detailed description.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Self for method chaining.
|
|
325
|
+
"""
|
|
326
|
+
self._description = description[:15000]
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
def add_responder(self, responder: Responder) -> "AlertPayloadBuilder":
|
|
330
|
+
"""Add a responder to the alert.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
responder: Responder to notify.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Self for method chaining.
|
|
337
|
+
"""
|
|
338
|
+
self._responders.append(responder)
|
|
339
|
+
return self
|
|
340
|
+
|
|
341
|
+
def add_responders(self, responders: list[Responder]) -> "AlertPayloadBuilder":
|
|
342
|
+
"""Add multiple responders.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
responders: List of responders to notify.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Self for method chaining.
|
|
349
|
+
"""
|
|
350
|
+
self._responders.extend(responders)
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
def add_visible_to(self, responder: Responder) -> "AlertPayloadBuilder":
|
|
354
|
+
"""Add a responder who can see the alert (without being notified).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
responder: Responder who can view the alert.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Self for method chaining.
|
|
361
|
+
"""
|
|
362
|
+
self._visible_to.append(responder)
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
def add_action(self, action: str) -> "AlertPayloadBuilder":
|
|
366
|
+
"""Add a custom action button.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
action: Action button text.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Self for method chaining.
|
|
373
|
+
"""
|
|
374
|
+
self._actions.append(action)
|
|
375
|
+
return self
|
|
376
|
+
|
|
377
|
+
def add_tag(self, tag: str) -> "AlertPayloadBuilder":
|
|
378
|
+
"""Add a tag to the alert.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
tag: Tag string.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Self for method chaining.
|
|
385
|
+
"""
|
|
386
|
+
self._tags.append(tag)
|
|
387
|
+
return self
|
|
388
|
+
|
|
389
|
+
def add_tags(self, tags: list[str]) -> "AlertPayloadBuilder":
|
|
390
|
+
"""Add multiple tags.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
tags: List of tags.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Self for method chaining.
|
|
397
|
+
"""
|
|
398
|
+
self._tags.extend(tags)
|
|
399
|
+
return self
|
|
400
|
+
|
|
401
|
+
def set_details(self, details: dict[str, Any]) -> "AlertPayloadBuilder":
|
|
402
|
+
"""Set custom details (key-value pairs, max 8000 chars total).
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
details: Dictionary of custom details.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Self for method chaining.
|
|
409
|
+
"""
|
|
410
|
+
self._details = {str(k): str(v) for k, v in details.items()}
|
|
411
|
+
return self
|
|
412
|
+
|
|
413
|
+
def add_detail(self, key: str, value: Any) -> "AlertPayloadBuilder":
|
|
414
|
+
"""Add a single custom detail.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
key: Detail key.
|
|
418
|
+
value: Detail value.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Self for method chaining.
|
|
422
|
+
"""
|
|
423
|
+
self._details[str(key)] = str(value)
|
|
424
|
+
return self
|
|
425
|
+
|
|
426
|
+
def set_entity(self, entity: str) -> "AlertPayloadBuilder":
|
|
427
|
+
"""Set the entity associated with the alert (max 512 chars).
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
entity: Entity name (e.g., service, host).
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Self for method chaining.
|
|
434
|
+
"""
|
|
435
|
+
self._entity = entity[:512]
|
|
436
|
+
return self
|
|
437
|
+
|
|
438
|
+
def set_source(self, source: str) -> "AlertPayloadBuilder":
|
|
439
|
+
"""Set the alert source (max 100 chars).
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
source: Source of the alert.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Self for method chaining.
|
|
446
|
+
"""
|
|
447
|
+
self._source = source[:100]
|
|
448
|
+
return self
|
|
449
|
+
|
|
450
|
+
def set_priority(self, priority: AlertPriority | str) -> "AlertPayloadBuilder":
|
|
451
|
+
"""Set the alert priority.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
priority: Priority level (P1-P5).
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Self for method chaining.
|
|
458
|
+
"""
|
|
459
|
+
if isinstance(priority, str):
|
|
460
|
+
priority = AlertPriority(priority.upper())
|
|
461
|
+
self._priority = priority
|
|
462
|
+
return self
|
|
463
|
+
|
|
464
|
+
def set_user(self, user: str) -> "AlertPayloadBuilder":
|
|
465
|
+
"""Set the user who triggered the alert.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
user: Username or email.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Self for method chaining.
|
|
472
|
+
"""
|
|
473
|
+
self._user = user
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
def set_note(self, note: str) -> "AlertPayloadBuilder":
|
|
477
|
+
"""Set a note to include with the alert.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
note: Note text.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Self for method chaining.
|
|
484
|
+
"""
|
|
485
|
+
self._note = note
|
|
486
|
+
return self
|
|
487
|
+
|
|
488
|
+
def build(self) -> dict[str, Any]:
|
|
489
|
+
"""Build the alert payload.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Dictionary representing the OpsGenie alert payload.
|
|
493
|
+
|
|
494
|
+
Raises:
|
|
495
|
+
ValueError: If required fields are missing.
|
|
496
|
+
"""
|
|
497
|
+
if not self._message:
|
|
498
|
+
raise ValueError("Message is required")
|
|
499
|
+
|
|
500
|
+
payload: dict[str, Any] = {
|
|
501
|
+
"message": self._message,
|
|
502
|
+
"priority": str(self._priority),
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if self._alias:
|
|
506
|
+
payload["alias"] = self._alias
|
|
507
|
+
if self._description:
|
|
508
|
+
payload["description"] = self._description
|
|
509
|
+
if self._responders:
|
|
510
|
+
payload["responders"] = [r.to_dict() for r in self._responders]
|
|
511
|
+
if self._visible_to:
|
|
512
|
+
payload["visibleTo"] = [r.to_dict() for r in self._visible_to]
|
|
513
|
+
if self._actions:
|
|
514
|
+
payload["actions"] = self._actions
|
|
515
|
+
if self._tags:
|
|
516
|
+
payload["tags"] = self._tags
|
|
517
|
+
if self._details:
|
|
518
|
+
payload["details"] = self._details
|
|
519
|
+
if self._entity:
|
|
520
|
+
payload["entity"] = self._entity
|
|
521
|
+
if self._source:
|
|
522
|
+
payload["source"] = self._source
|
|
523
|
+
if self._user:
|
|
524
|
+
payload["user"] = self._user
|
|
525
|
+
if self._note:
|
|
526
|
+
payload["note"] = self._note
|
|
527
|
+
|
|
528
|
+
return payload
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# =============================================================================
|
|
532
|
+
# Message Templates (Strategy Pattern)
|
|
533
|
+
# =============================================================================
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class AlertTemplate(ABC):
|
|
537
|
+
"""Abstract base class for alert message templates.
|
|
538
|
+
|
|
539
|
+
Templates define how checkpoint results are formatted into OpsGenie alerts.
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
@abstractmethod
|
|
543
|
+
def build_payload(
|
|
544
|
+
self,
|
|
545
|
+
checkpoint_result: "CheckpointResult",
|
|
546
|
+
config: "OpsGenieConfig",
|
|
547
|
+
) -> dict[str, Any]:
|
|
548
|
+
"""Build alert payload from checkpoint result.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
checkpoint_result: The validation result.
|
|
552
|
+
config: OpsGenie configuration.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Alert payload dictionary.
|
|
556
|
+
"""
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class DefaultAlertTemplate(AlertTemplate):
|
|
561
|
+
"""Default alert template with comprehensive details."""
|
|
562
|
+
|
|
563
|
+
def build_payload(
|
|
564
|
+
self,
|
|
565
|
+
checkpoint_result: "CheckpointResult",
|
|
566
|
+
config: "OpsGenieConfig",
|
|
567
|
+
) -> dict[str, Any]:
|
|
568
|
+
"""Build default alert payload."""
|
|
569
|
+
validation = checkpoint_result.validation_result
|
|
570
|
+
stats = validation.statistics if validation else None
|
|
571
|
+
status = checkpoint_result.status.value
|
|
572
|
+
|
|
573
|
+
# Build message
|
|
574
|
+
message = (
|
|
575
|
+
f"[{status.upper()}] {checkpoint_result.checkpoint_name} - "
|
|
576
|
+
f"{checkpoint_result.data_asset}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Build description
|
|
580
|
+
description_parts = [
|
|
581
|
+
f"**Checkpoint**: {checkpoint_result.checkpoint_name}",
|
|
582
|
+
f"**Data Asset**: {checkpoint_result.data_asset}",
|
|
583
|
+
f"**Status**: {status}",
|
|
584
|
+
f"**Run ID**: {checkpoint_result.run_id}",
|
|
585
|
+
f"**Time**: {checkpoint_result.run_time.isoformat()}",
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
if stats:
|
|
589
|
+
description_parts.extend([
|
|
590
|
+
"",
|
|
591
|
+
"**Statistics**:",
|
|
592
|
+
f"- Total Issues: {stats.total_issues}",
|
|
593
|
+
f"- Critical: {stats.critical_issues}",
|
|
594
|
+
f"- High: {stats.high_issues}",
|
|
595
|
+
f"- Medium: {stats.medium_issues}",
|
|
596
|
+
f"- Low: {stats.low_issues}",
|
|
597
|
+
f"- Pass Rate: {stats.pass_rate * 100:.1f}%",
|
|
598
|
+
])
|
|
599
|
+
|
|
600
|
+
description = "\n".join(description_parts)
|
|
601
|
+
|
|
602
|
+
# Determine priority
|
|
603
|
+
priority = self._determine_priority(stats, config)
|
|
604
|
+
|
|
605
|
+
# Build alias for deduplication
|
|
606
|
+
alias = config.alias_template.format(
|
|
607
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
608
|
+
data_asset=checkpoint_result.data_asset,
|
|
609
|
+
run_id=checkpoint_result.run_id,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Build payload using builder
|
|
613
|
+
builder = (
|
|
614
|
+
AlertPayloadBuilder()
|
|
615
|
+
.set_message(message)
|
|
616
|
+
.set_alias(alias)
|
|
617
|
+
.set_description(description)
|
|
618
|
+
.set_priority(priority)
|
|
619
|
+
.set_entity(checkpoint_result.data_asset or "truthound")
|
|
620
|
+
.set_source("truthound")
|
|
621
|
+
.add_tags(list(config.tags))
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Add responders
|
|
625
|
+
for responder in config.responders:
|
|
626
|
+
if isinstance(responder, Responder):
|
|
627
|
+
builder.add_responder(responder)
|
|
628
|
+
elif isinstance(responder, dict):
|
|
629
|
+
builder.add_responder(Responder(**responder))
|
|
630
|
+
|
|
631
|
+
# Add custom details
|
|
632
|
+
details = {
|
|
633
|
+
"checkpoint": checkpoint_result.checkpoint_name,
|
|
634
|
+
"data_asset": checkpoint_result.data_asset or "",
|
|
635
|
+
"run_id": checkpoint_result.run_id,
|
|
636
|
+
"status": status,
|
|
637
|
+
}
|
|
638
|
+
if stats:
|
|
639
|
+
details.update({
|
|
640
|
+
"total_issues": str(stats.total_issues),
|
|
641
|
+
"critical_issues": str(stats.critical_issues),
|
|
642
|
+
"high_issues": str(stats.high_issues),
|
|
643
|
+
"medium_issues": str(stats.medium_issues),
|
|
644
|
+
"low_issues": str(stats.low_issues),
|
|
645
|
+
"pass_rate": f"{stats.pass_rate * 100:.1f}%",
|
|
646
|
+
})
|
|
647
|
+
details.update(config.custom_details)
|
|
648
|
+
builder.set_details(details)
|
|
649
|
+
|
|
650
|
+
return builder.build()
|
|
651
|
+
|
|
652
|
+
def _determine_priority(
|
|
653
|
+
self,
|
|
654
|
+
stats: Any,
|
|
655
|
+
config: "OpsGenieConfig",
|
|
656
|
+
) -> AlertPriority:
|
|
657
|
+
"""Determine alert priority based on validation statistics."""
|
|
658
|
+
if not config.auto_priority or not stats:
|
|
659
|
+
return config.priority
|
|
660
|
+
|
|
661
|
+
if stats.critical_issues > 0:
|
|
662
|
+
return AlertPriority.P1
|
|
663
|
+
elif stats.high_issues > 0:
|
|
664
|
+
return AlertPriority.P2
|
|
665
|
+
elif stats.medium_issues > 0:
|
|
666
|
+
return AlertPriority.P3
|
|
667
|
+
elif stats.low_issues > 0:
|
|
668
|
+
return AlertPriority.P4
|
|
669
|
+
else:
|
|
670
|
+
return AlertPriority.P5
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class MinimalAlertTemplate(AlertTemplate):
|
|
674
|
+
"""Minimal alert template with only essential information."""
|
|
675
|
+
|
|
676
|
+
def build_payload(
|
|
677
|
+
self,
|
|
678
|
+
checkpoint_result: "CheckpointResult",
|
|
679
|
+
config: "OpsGenieConfig",
|
|
680
|
+
) -> dict[str, Any]:
|
|
681
|
+
"""Build minimal alert payload."""
|
|
682
|
+
status = checkpoint_result.status.value
|
|
683
|
+
stats = checkpoint_result.validation_result.statistics if checkpoint_result.validation_result else None
|
|
684
|
+
|
|
685
|
+
message = f"[{status.upper()}] {checkpoint_result.checkpoint_name}"
|
|
686
|
+
if stats and stats.total_issues > 0:
|
|
687
|
+
message += f" ({stats.total_issues} issues)"
|
|
688
|
+
|
|
689
|
+
alias = config.alias_template.format(
|
|
690
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
691
|
+
data_asset=checkpoint_result.data_asset or "",
|
|
692
|
+
run_id=checkpoint_result.run_id,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
builder = (
|
|
696
|
+
AlertPayloadBuilder()
|
|
697
|
+
.set_message(message)
|
|
698
|
+
.set_alias(alias)
|
|
699
|
+
.set_priority(config.priority)
|
|
700
|
+
.set_source("truthound")
|
|
701
|
+
.add_tags(list(config.tags))
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
for responder in config.responders:
|
|
705
|
+
if isinstance(responder, Responder):
|
|
706
|
+
builder.add_responder(responder)
|
|
707
|
+
elif isinstance(responder, dict):
|
|
708
|
+
builder.add_responder(Responder(**responder))
|
|
709
|
+
|
|
710
|
+
return builder.build()
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class DetailedAlertTemplate(AlertTemplate):
|
|
714
|
+
"""Detailed alert template with extended information and recommendations."""
|
|
715
|
+
|
|
716
|
+
def build_payload(
|
|
717
|
+
self,
|
|
718
|
+
checkpoint_result: "CheckpointResult",
|
|
719
|
+
config: "OpsGenieConfig",
|
|
720
|
+
) -> dict[str, Any]:
|
|
721
|
+
"""Build detailed alert payload."""
|
|
722
|
+
validation = checkpoint_result.validation_result
|
|
723
|
+
stats = validation.statistics if validation else None
|
|
724
|
+
status = checkpoint_result.status.value
|
|
725
|
+
|
|
726
|
+
# Build message
|
|
727
|
+
message = f"Data Quality Alert: {checkpoint_result.checkpoint_name}"
|
|
728
|
+
|
|
729
|
+
# Build comprehensive description
|
|
730
|
+
description_parts = [
|
|
731
|
+
"# Data Quality Validation Alert",
|
|
732
|
+
"",
|
|
733
|
+
"## Overview",
|
|
734
|
+
f"- **Checkpoint**: {checkpoint_result.checkpoint_name}",
|
|
735
|
+
f"- **Data Asset**: {checkpoint_result.data_asset}",
|
|
736
|
+
f"- **Status**: {status.upper()}",
|
|
737
|
+
f"- **Run ID**: {checkpoint_result.run_id}",
|
|
738
|
+
f"- **Timestamp**: {checkpoint_result.run_time.isoformat()}",
|
|
739
|
+
]
|
|
740
|
+
|
|
741
|
+
if stats:
|
|
742
|
+
description_parts.extend([
|
|
743
|
+
"",
|
|
744
|
+
"## Validation Statistics",
|
|
745
|
+
f"| Metric | Value |",
|
|
746
|
+
f"|--------|-------|",
|
|
747
|
+
f"| Total Issues | {stats.total_issues} |",
|
|
748
|
+
f"| Critical | {stats.critical_issues} |",
|
|
749
|
+
f"| High | {stats.high_issues} |",
|
|
750
|
+
f"| Medium | {stats.medium_issues} |",
|
|
751
|
+
f"| Low | {stats.low_issues} |",
|
|
752
|
+
f"| Pass Rate | {stats.pass_rate * 100:.1f}% |",
|
|
753
|
+
])
|
|
754
|
+
|
|
755
|
+
# Add failed validations if available
|
|
756
|
+
if validation and validation.results:
|
|
757
|
+
failed_results = [r for r in validation.results if not r.passed]
|
|
758
|
+
if failed_results:
|
|
759
|
+
description_parts.extend([
|
|
760
|
+
"",
|
|
761
|
+
"## Failed Validations",
|
|
762
|
+
])
|
|
763
|
+
for i, result in enumerate(failed_results[:10], 1):
|
|
764
|
+
description_parts.append(
|
|
765
|
+
f"{i}. **{result.validator_name}** on `{result.column}`: {result.message}"
|
|
766
|
+
)
|
|
767
|
+
if len(failed_results) > 10:
|
|
768
|
+
description_parts.append(f"... and {len(failed_results) - 10} more")
|
|
769
|
+
|
|
770
|
+
description_parts.extend([
|
|
771
|
+
"",
|
|
772
|
+
"## Recommended Actions",
|
|
773
|
+
"1. Review the validation results in detail",
|
|
774
|
+
"2. Investigate the root cause of failures",
|
|
775
|
+
"3. Apply necessary data fixes",
|
|
776
|
+
"4. Re-run validation to confirm resolution",
|
|
777
|
+
])
|
|
778
|
+
|
|
779
|
+
description = "\n".join(description_parts)
|
|
780
|
+
|
|
781
|
+
# Determine priority
|
|
782
|
+
priority = self._determine_priority(stats, config)
|
|
783
|
+
|
|
784
|
+
alias = config.alias_template.format(
|
|
785
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
786
|
+
data_asset=checkpoint_result.data_asset or "",
|
|
787
|
+
run_id=checkpoint_result.run_id,
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
builder = (
|
|
791
|
+
AlertPayloadBuilder()
|
|
792
|
+
.set_message(message)
|
|
793
|
+
.set_alias(alias)
|
|
794
|
+
.set_description(description)
|
|
795
|
+
.set_priority(priority)
|
|
796
|
+
.set_entity(checkpoint_result.data_asset or "truthound")
|
|
797
|
+
.set_source("truthound")
|
|
798
|
+
.add_tags(list(config.tags))
|
|
799
|
+
.add_action("View Dashboard")
|
|
800
|
+
.add_action("Acknowledge")
|
|
801
|
+
.add_action("Escalate")
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
for responder in config.responders:
|
|
805
|
+
if isinstance(responder, Responder):
|
|
806
|
+
builder.add_responder(responder)
|
|
807
|
+
elif isinstance(responder, dict):
|
|
808
|
+
builder.add_responder(Responder(**responder))
|
|
809
|
+
|
|
810
|
+
# Extended details
|
|
811
|
+
details = {
|
|
812
|
+
"checkpoint": checkpoint_result.checkpoint_name,
|
|
813
|
+
"data_asset": checkpoint_result.data_asset or "",
|
|
814
|
+
"run_id": checkpoint_result.run_id,
|
|
815
|
+
"status": status,
|
|
816
|
+
"environment": config.custom_details.get("environment", "unknown"),
|
|
817
|
+
}
|
|
818
|
+
if stats:
|
|
819
|
+
details.update({
|
|
820
|
+
"total_issues": str(stats.total_issues),
|
|
821
|
+
"critical_issues": str(stats.critical_issues),
|
|
822
|
+
"pass_rate": f"{stats.pass_rate * 100:.1f}%",
|
|
823
|
+
})
|
|
824
|
+
details.update(config.custom_details)
|
|
825
|
+
builder.set_details(details)
|
|
826
|
+
|
|
827
|
+
return builder.build()
|
|
828
|
+
|
|
829
|
+
def _determine_priority(
|
|
830
|
+
self,
|
|
831
|
+
stats: Any,
|
|
832
|
+
config: "OpsGenieConfig",
|
|
833
|
+
) -> AlertPriority:
|
|
834
|
+
"""Determine alert priority based on validation statistics."""
|
|
835
|
+
if not config.auto_priority or not stats:
|
|
836
|
+
return config.priority
|
|
837
|
+
|
|
838
|
+
if stats.critical_issues > 0:
|
|
839
|
+
return AlertPriority.P1
|
|
840
|
+
elif stats.high_issues > 0:
|
|
841
|
+
return AlertPriority.P2
|
|
842
|
+
elif stats.medium_issues > 0:
|
|
843
|
+
return AlertPriority.P3
|
|
844
|
+
else:
|
|
845
|
+
return AlertPriority.P4
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
# Template Registry
|
|
849
|
+
_TEMPLATE_REGISTRY: dict[str, type[AlertTemplate]] = {
|
|
850
|
+
"default": DefaultAlertTemplate,
|
|
851
|
+
"minimal": MinimalAlertTemplate,
|
|
852
|
+
"detailed": DetailedAlertTemplate,
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def register_template(name: str, template_class: type[AlertTemplate]) -> None:
|
|
857
|
+
"""Register a custom alert template.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
name: Template name for lookup.
|
|
861
|
+
template_class: AlertTemplate subclass.
|
|
862
|
+
"""
|
|
863
|
+
_TEMPLATE_REGISTRY[name.lower()] = template_class
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def get_template(name: str) -> AlertTemplate:
|
|
867
|
+
"""Get a template instance by name.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
name: Template name.
|
|
871
|
+
|
|
872
|
+
Returns:
|
|
873
|
+
AlertTemplate instance.
|
|
874
|
+
|
|
875
|
+
Raises:
|
|
876
|
+
ValueError: If template is not found.
|
|
877
|
+
"""
|
|
878
|
+
template_class = _TEMPLATE_REGISTRY.get(name.lower())
|
|
879
|
+
if not template_class:
|
|
880
|
+
available = ", ".join(_TEMPLATE_REGISTRY.keys())
|
|
881
|
+
raise ValueError(f"Unknown template '{name}'. Available: {available}")
|
|
882
|
+
return template_class()
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
# =============================================================================
|
|
886
|
+
# HTTP Client Abstraction
|
|
887
|
+
# =============================================================================
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
class OpsGenieHTTPClient:
|
|
891
|
+
"""HTTP client for OpsGenie API calls.
|
|
892
|
+
|
|
893
|
+
This abstraction allows for easier testing and customization.
|
|
894
|
+
"""
|
|
895
|
+
|
|
896
|
+
def __init__(
|
|
897
|
+
self,
|
|
898
|
+
api_key: str,
|
|
899
|
+
region: OpsGenieRegion = OpsGenieRegion.US,
|
|
900
|
+
timeout: int = 30,
|
|
901
|
+
proxy: str | None = None,
|
|
902
|
+
verify_ssl: bool = True,
|
|
903
|
+
) -> None:
|
|
904
|
+
"""Initialize the HTTP client.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
api_key: OpsGenie API key.
|
|
908
|
+
region: API region (US or EU).
|
|
909
|
+
timeout: Request timeout in seconds.
|
|
910
|
+
proxy: Optional proxy URL.
|
|
911
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
912
|
+
"""
|
|
913
|
+
self._api_key = api_key
|
|
914
|
+
self._region = region
|
|
915
|
+
self._timeout = timeout
|
|
916
|
+
self._proxy = proxy
|
|
917
|
+
self._verify_ssl = verify_ssl
|
|
918
|
+
|
|
919
|
+
@property
|
|
920
|
+
def base_url(self) -> str:
|
|
921
|
+
"""Get the base API URL."""
|
|
922
|
+
return self._region.api_url
|
|
923
|
+
|
|
924
|
+
def _get_headers(self) -> dict[str, str]:
|
|
925
|
+
"""Get request headers."""
|
|
926
|
+
return {
|
|
927
|
+
"Authorization": f"GenieKey {self._api_key}",
|
|
928
|
+
"Content-Type": "application/json",
|
|
929
|
+
"User-Agent": "Truthound/1.0",
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
def post(self, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
933
|
+
"""Send POST request to OpsGenie API.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
endpoint: API endpoint path.
|
|
937
|
+
payload: Request payload.
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
Response data.
|
|
941
|
+
|
|
942
|
+
Raises:
|
|
943
|
+
OpsGenieAPIError: If the request fails.
|
|
944
|
+
"""
|
|
945
|
+
import urllib.request
|
|
946
|
+
import urllib.error
|
|
947
|
+
import ssl
|
|
948
|
+
|
|
949
|
+
url = f"{self.base_url}{endpoint}"
|
|
950
|
+
data = json.dumps(payload).encode("utf-8")
|
|
951
|
+
headers = self._get_headers()
|
|
952
|
+
|
|
953
|
+
request = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
954
|
+
|
|
955
|
+
# Configure SSL
|
|
956
|
+
context: ssl.SSLContext | None = None
|
|
957
|
+
if not self._verify_ssl:
|
|
958
|
+
context = ssl.create_default_context()
|
|
959
|
+
context.check_hostname = False
|
|
960
|
+
context.verify_mode = ssl.CERT_NONE
|
|
961
|
+
|
|
962
|
+
# Configure proxy
|
|
963
|
+
if self._proxy:
|
|
964
|
+
proxy_handler = urllib.request.ProxyHandler({"https": self._proxy})
|
|
965
|
+
opener = urllib.request.build_opener(proxy_handler)
|
|
966
|
+
else:
|
|
967
|
+
opener = urllib.request.build_opener()
|
|
968
|
+
|
|
969
|
+
try:
|
|
970
|
+
with opener.open(request, timeout=self._timeout, context=context) as response:
|
|
971
|
+
response_body = response.read().decode("utf-8")
|
|
972
|
+
return json.loads(response_body) if response_body else {}
|
|
973
|
+
except urllib.error.HTTPError as e:
|
|
974
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
975
|
+
raise OpsGenieAPIError(
|
|
976
|
+
f"HTTP {e.code}: {e.reason}",
|
|
977
|
+
status_code=e.code,
|
|
978
|
+
response_body=error_body,
|
|
979
|
+
) from e
|
|
980
|
+
except urllib.error.URLError as e:
|
|
981
|
+
raise OpsGenieAPIError(f"URL Error: {e.reason}") from e
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
class OpsGenieAPIError(Exception):
|
|
985
|
+
"""Exception for OpsGenie API errors."""
|
|
986
|
+
|
|
987
|
+
def __init__(
|
|
988
|
+
self,
|
|
989
|
+
message: str,
|
|
990
|
+
status_code: int | None = None,
|
|
991
|
+
response_body: str | None = None,
|
|
992
|
+
) -> None:
|
|
993
|
+
"""Initialize the error.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
message: Error message.
|
|
997
|
+
status_code: HTTP status code.
|
|
998
|
+
response_body: Raw response body.
|
|
999
|
+
"""
|
|
1000
|
+
super().__init__(message)
|
|
1001
|
+
self.status_code = status_code
|
|
1002
|
+
self.response_body = response_body
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
# =============================================================================
|
|
1006
|
+
# Configuration
|
|
1007
|
+
# =============================================================================
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@dataclass
|
|
1011
|
+
class OpsGenieConfig(ActionConfig):
|
|
1012
|
+
"""Configuration for OpsGenie action.
|
|
1013
|
+
|
|
1014
|
+
Attributes:
|
|
1015
|
+
api_key: OpsGenie API key (required).
|
|
1016
|
+
region: API region (us, eu).
|
|
1017
|
+
priority: Default alert priority (P1-P5).
|
|
1018
|
+
auto_priority: Automatically map validation severity to priority.
|
|
1019
|
+
responders: List of responders to notify.
|
|
1020
|
+
visible_to: List of responders who can view but not be notified.
|
|
1021
|
+
tags: Tags to attach to alerts.
|
|
1022
|
+
alias_template: Template for generating alert aliases (dedup).
|
|
1023
|
+
close_on_success: Close matching alerts on validation success.
|
|
1024
|
+
acknowledge_on_warning: Acknowledge alerts on warning status.
|
|
1025
|
+
template: Alert template to use (default, minimal, detailed).
|
|
1026
|
+
custom_template: Custom AlertTemplate instance.
|
|
1027
|
+
custom_details: Additional details to include in alerts.
|
|
1028
|
+
source: Source identifier for alerts.
|
|
1029
|
+
entity: Entity associated with alerts.
|
|
1030
|
+
actions: Custom action buttons to include.
|
|
1031
|
+
proxy: Optional proxy URL.
|
|
1032
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
1033
|
+
"""
|
|
1034
|
+
|
|
1035
|
+
api_key: str = ""
|
|
1036
|
+
region: OpsGenieRegion | str = OpsGenieRegion.US
|
|
1037
|
+
priority: AlertPriority | str = AlertPriority.P3
|
|
1038
|
+
auto_priority: bool = True
|
|
1039
|
+
responders: list[Responder | dict[str, str]] = field(default_factory=list)
|
|
1040
|
+
visible_to: list[Responder | dict[str, str]] = field(default_factory=list)
|
|
1041
|
+
tags: list[str] = field(default_factory=lambda: ["truthound", "data-quality"])
|
|
1042
|
+
alias_template: str = "truthound_{checkpoint}_{data_asset}"
|
|
1043
|
+
close_on_success: bool = True
|
|
1044
|
+
acknowledge_on_warning: bool = False
|
|
1045
|
+
template: str = "default"
|
|
1046
|
+
custom_template: AlertTemplate | None = None
|
|
1047
|
+
custom_details: dict[str, str] = field(default_factory=dict)
|
|
1048
|
+
source: str = "truthound"
|
|
1049
|
+
entity: str | None = None
|
|
1050
|
+
actions: list[str] = field(default_factory=list)
|
|
1051
|
+
proxy: str | None = None
|
|
1052
|
+
verify_ssl: bool = True
|
|
1053
|
+
notify_on: NotifyCondition | str = NotifyCondition.FAILURE_OR_ERROR
|
|
1054
|
+
|
|
1055
|
+
def __post_init__(self) -> None:
|
|
1056
|
+
"""Normalize configuration values."""
|
|
1057
|
+
super().__post_init__()
|
|
1058
|
+
|
|
1059
|
+
if isinstance(self.region, str):
|
|
1060
|
+
self.region = OpsGenieRegion(self.region.lower())
|
|
1061
|
+
if isinstance(self.priority, str):
|
|
1062
|
+
self.priority = AlertPriority(self.priority.upper())
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
# =============================================================================
|
|
1066
|
+
# Main Action Class
|
|
1067
|
+
# =============================================================================
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
class OpsGenieAction(BaseAction[OpsGenieConfig]):
|
|
1071
|
+
"""Action to create and manage OpsGenie alerts.
|
|
1072
|
+
|
|
1073
|
+
Creates, closes, or acknowledges alerts in OpsGenie based on validation
|
|
1074
|
+
results using the Alert API v2.
|
|
1075
|
+
|
|
1076
|
+
Features:
|
|
1077
|
+
- Full OpsGenie Alert API v2 support
|
|
1078
|
+
- Multiple alert actions (create, close, acknowledge)
|
|
1079
|
+
- Flexible responder configuration
|
|
1080
|
+
- Auto-priority mapping from validation severity
|
|
1081
|
+
- Deduplication for alert grouping
|
|
1082
|
+
- Extensible template system
|
|
1083
|
+
|
|
1084
|
+
Example:
|
|
1085
|
+
>>> # Basic usage
|
|
1086
|
+
>>> action = OpsGenieAction(
|
|
1087
|
+
... api_key="your-api-key",
|
|
1088
|
+
... notify_on="failure",
|
|
1089
|
+
... )
|
|
1090
|
+
>>> result = action.execute(checkpoint_result)
|
|
1091
|
+
|
|
1092
|
+
>>> # With team responders
|
|
1093
|
+
>>> action = OpsGenieAction(
|
|
1094
|
+
... api_key="your-api-key",
|
|
1095
|
+
... responders=[
|
|
1096
|
+
... Responder.team("data-quality-team"),
|
|
1097
|
+
... Responder.user("admin@example.com"),
|
|
1098
|
+
... ],
|
|
1099
|
+
... auto_priority=True,
|
|
1100
|
+
... tags=["production", "critical-pipeline"],
|
|
1101
|
+
... )
|
|
1102
|
+
|
|
1103
|
+
>>> # Using custom template
|
|
1104
|
+
>>> action = OpsGenieAction(
|
|
1105
|
+
... api_key="your-api-key",
|
|
1106
|
+
... template="detailed",
|
|
1107
|
+
... close_on_success=True,
|
|
1108
|
+
... )
|
|
1109
|
+
"""
|
|
1110
|
+
|
|
1111
|
+
action_type = "opsgenie"
|
|
1112
|
+
|
|
1113
|
+
def __init__(
|
|
1114
|
+
self,
|
|
1115
|
+
config: OpsGenieConfig | None = None,
|
|
1116
|
+
client: OpsGenieHTTPClient | None = None,
|
|
1117
|
+
**kwargs: Any,
|
|
1118
|
+
) -> None:
|
|
1119
|
+
"""Initialize the action.
|
|
1120
|
+
|
|
1121
|
+
Args:
|
|
1122
|
+
config: OpsGenie configuration.
|
|
1123
|
+
client: Optional HTTP client (for testing).
|
|
1124
|
+
**kwargs: Additional configuration options.
|
|
1125
|
+
"""
|
|
1126
|
+
super().__init__(config, **kwargs)
|
|
1127
|
+
self._client = client
|
|
1128
|
+
|
|
1129
|
+
@classmethod
|
|
1130
|
+
def _default_config(cls) -> OpsGenieConfig:
|
|
1131
|
+
"""Create default configuration."""
|
|
1132
|
+
return OpsGenieConfig()
|
|
1133
|
+
|
|
1134
|
+
def _get_client(self) -> OpsGenieHTTPClient:
|
|
1135
|
+
"""Get or create HTTP client."""
|
|
1136
|
+
if self._client:
|
|
1137
|
+
return self._client
|
|
1138
|
+
|
|
1139
|
+
config = self._config
|
|
1140
|
+
return OpsGenieHTTPClient(
|
|
1141
|
+
api_key=config.api_key,
|
|
1142
|
+
region=config.region if isinstance(config.region, OpsGenieRegion) else OpsGenieRegion(config.region),
|
|
1143
|
+
timeout=config.timeout_seconds,
|
|
1144
|
+
proxy=config.proxy,
|
|
1145
|
+
verify_ssl=config.verify_ssl,
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
def _get_template(self) -> AlertTemplate:
|
|
1149
|
+
"""Get the configured alert template."""
|
|
1150
|
+
if self._config.custom_template:
|
|
1151
|
+
return self._config.custom_template
|
|
1152
|
+
return get_template(self._config.template)
|
|
1153
|
+
|
|
1154
|
+
def _execute(self, checkpoint_result: "CheckpointResult") -> ActionResult:
|
|
1155
|
+
"""Execute the OpsGenie action."""
|
|
1156
|
+
config = self._config
|
|
1157
|
+
|
|
1158
|
+
if not config.api_key:
|
|
1159
|
+
return ActionResult(
|
|
1160
|
+
action_name=self.name,
|
|
1161
|
+
action_type=self.action_type,
|
|
1162
|
+
status=ActionStatus.ERROR,
|
|
1163
|
+
message="No API key configured",
|
|
1164
|
+
error="api_key is required",
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
status = checkpoint_result.status.value
|
|
1168
|
+
client = self._get_client()
|
|
1169
|
+
|
|
1170
|
+
try:
|
|
1171
|
+
# Determine action based on status
|
|
1172
|
+
if status == "success" and config.close_on_success:
|
|
1173
|
+
return self._close_alert(checkpoint_result, client)
|
|
1174
|
+
elif status == "warning" and config.acknowledge_on_warning:
|
|
1175
|
+
return self._acknowledge_alert(checkpoint_result, client)
|
|
1176
|
+
elif status in ("failure", "error"):
|
|
1177
|
+
return self._create_alert(checkpoint_result, client)
|
|
1178
|
+
else:
|
|
1179
|
+
# Create alert for other statuses
|
|
1180
|
+
return self._create_alert(checkpoint_result, client)
|
|
1181
|
+
|
|
1182
|
+
except OpsGenieAPIError as e:
|
|
1183
|
+
return ActionResult(
|
|
1184
|
+
action_name=self.name,
|
|
1185
|
+
action_type=self.action_type,
|
|
1186
|
+
status=ActionStatus.ERROR,
|
|
1187
|
+
message="OpsGenie API error",
|
|
1188
|
+
error=str(e),
|
|
1189
|
+
details={
|
|
1190
|
+
"status_code": e.status_code,
|
|
1191
|
+
"response": e.response_body,
|
|
1192
|
+
},
|
|
1193
|
+
)
|
|
1194
|
+
except Exception as e:
|
|
1195
|
+
return ActionResult(
|
|
1196
|
+
action_name=self.name,
|
|
1197
|
+
action_type=self.action_type,
|
|
1198
|
+
status=ActionStatus.ERROR,
|
|
1199
|
+
message="Failed to send OpsGenie alert",
|
|
1200
|
+
error=str(e),
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
def _create_alert(
|
|
1204
|
+
self,
|
|
1205
|
+
checkpoint_result: "CheckpointResult",
|
|
1206
|
+
client: OpsGenieHTTPClient,
|
|
1207
|
+
) -> ActionResult:
|
|
1208
|
+
"""Create a new alert."""
|
|
1209
|
+
template = self._get_template()
|
|
1210
|
+
payload = template.build_payload(checkpoint_result, self._config)
|
|
1211
|
+
|
|
1212
|
+
response = client.post("/v2/alerts", payload)
|
|
1213
|
+
|
|
1214
|
+
return ActionResult(
|
|
1215
|
+
action_name=self.name,
|
|
1216
|
+
action_type=self.action_type,
|
|
1217
|
+
status=ActionStatus.SUCCESS,
|
|
1218
|
+
message="OpsGenie alert created",
|
|
1219
|
+
details={
|
|
1220
|
+
"action": "create",
|
|
1221
|
+
"alias": payload.get("alias"),
|
|
1222
|
+
"priority": payload.get("priority"),
|
|
1223
|
+
"request_id": response.get("requestId"),
|
|
1224
|
+
"result": response.get("result"),
|
|
1225
|
+
},
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
def _close_alert(
|
|
1229
|
+
self,
|
|
1230
|
+
checkpoint_result: "CheckpointResult",
|
|
1231
|
+
client: OpsGenieHTTPClient,
|
|
1232
|
+
) -> ActionResult:
|
|
1233
|
+
"""Close an existing alert."""
|
|
1234
|
+
alias = self._config.alias_template.format(
|
|
1235
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
1236
|
+
data_asset=checkpoint_result.data_asset or "",
|
|
1237
|
+
run_id=checkpoint_result.run_id,
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
payload = {
|
|
1241
|
+
"source": self._config.source,
|
|
1242
|
+
"user": "truthound",
|
|
1243
|
+
"note": f"Validation succeeded at {checkpoint_result.run_time.isoformat()}",
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
# Use alias identifier
|
|
1247
|
+
endpoint = f"/v2/alerts/{alias}/close?identifierType=alias"
|
|
1248
|
+
response = client.post(endpoint, payload)
|
|
1249
|
+
|
|
1250
|
+
return ActionResult(
|
|
1251
|
+
action_name=self.name,
|
|
1252
|
+
action_type=self.action_type,
|
|
1253
|
+
status=ActionStatus.SUCCESS,
|
|
1254
|
+
message="OpsGenie alert closed",
|
|
1255
|
+
details={
|
|
1256
|
+
"action": "close",
|
|
1257
|
+
"alias": alias,
|
|
1258
|
+
"request_id": response.get("requestId"),
|
|
1259
|
+
},
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
def _acknowledge_alert(
|
|
1263
|
+
self,
|
|
1264
|
+
checkpoint_result: "CheckpointResult",
|
|
1265
|
+
client: OpsGenieHTTPClient,
|
|
1266
|
+
) -> ActionResult:
|
|
1267
|
+
"""Acknowledge an existing alert."""
|
|
1268
|
+
alias = self._config.alias_template.format(
|
|
1269
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
1270
|
+
data_asset=checkpoint_result.data_asset or "",
|
|
1271
|
+
run_id=checkpoint_result.run_id,
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
payload = {
|
|
1275
|
+
"source": self._config.source,
|
|
1276
|
+
"user": "truthound",
|
|
1277
|
+
"note": f"Validation warning at {checkpoint_result.run_time.isoformat()}",
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
endpoint = f"/v2/alerts/{alias}/acknowledge?identifierType=alias"
|
|
1281
|
+
response = client.post(endpoint, payload)
|
|
1282
|
+
|
|
1283
|
+
return ActionResult(
|
|
1284
|
+
action_name=self.name,
|
|
1285
|
+
action_type=self.action_type,
|
|
1286
|
+
status=ActionStatus.SUCCESS,
|
|
1287
|
+
message="OpsGenie alert acknowledged",
|
|
1288
|
+
details={
|
|
1289
|
+
"action": "acknowledge",
|
|
1290
|
+
"alias": alias,
|
|
1291
|
+
"request_id": response.get("requestId"),
|
|
1292
|
+
},
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
def validate_config(self) -> list[str]:
|
|
1296
|
+
"""Validate the configuration."""
|
|
1297
|
+
errors = []
|
|
1298
|
+
|
|
1299
|
+
if not self._config.api_key:
|
|
1300
|
+
errors.append("api_key is required")
|
|
1301
|
+
|
|
1302
|
+
priority = self._config.priority
|
|
1303
|
+
if isinstance(priority, str):
|
|
1304
|
+
try:
|
|
1305
|
+
AlertPriority(priority.upper())
|
|
1306
|
+
except ValueError:
|
|
1307
|
+
errors.append(f"Invalid priority: {priority}. Must be P1-P5.")
|
|
1308
|
+
|
|
1309
|
+
region = self._config.region
|
|
1310
|
+
if isinstance(region, str):
|
|
1311
|
+
try:
|
|
1312
|
+
OpsGenieRegion(region.lower())
|
|
1313
|
+
except ValueError:
|
|
1314
|
+
errors.append(f"Invalid region: {region}. Must be 'us' or 'eu'.")
|
|
1315
|
+
|
|
1316
|
+
# Validate responders
|
|
1317
|
+
for i, responder in enumerate(self._config.responders):
|
|
1318
|
+
if isinstance(responder, dict):
|
|
1319
|
+
if "type" not in responder:
|
|
1320
|
+
errors.append(f"Responder {i}: 'type' is required")
|
|
1321
|
+
elif responder["type"] not in ("user", "team", "escalation", "schedule"):
|
|
1322
|
+
errors.append(f"Responder {i}: Invalid type '{responder['type']}'")
|
|
1323
|
+
|
|
1324
|
+
return errors
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
# =============================================================================
|
|
1328
|
+
# Convenience Functions
|
|
1329
|
+
# =============================================================================
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def create_opsgenie_action(
|
|
1333
|
+
api_key: str,
|
|
1334
|
+
*,
|
|
1335
|
+
notify_on: NotifyCondition | str = NotifyCondition.FAILURE_OR_ERROR,
|
|
1336
|
+
region: OpsGenieRegion | str = OpsGenieRegion.US,
|
|
1337
|
+
priority: AlertPriority | str = AlertPriority.P3,
|
|
1338
|
+
auto_priority: bool = True,
|
|
1339
|
+
template: str = "default",
|
|
1340
|
+
**kwargs: Any,
|
|
1341
|
+
) -> OpsGenieAction:
|
|
1342
|
+
"""Create an OpsGenie action with common settings.
|
|
1343
|
+
|
|
1344
|
+
Args:
|
|
1345
|
+
api_key: OpsGenie API key.
|
|
1346
|
+
notify_on: When to send notifications.
|
|
1347
|
+
region: API region.
|
|
1348
|
+
priority: Default priority.
|
|
1349
|
+
auto_priority: Auto-map severity to priority.
|
|
1350
|
+
template: Alert template to use.
|
|
1351
|
+
**kwargs: Additional configuration options.
|
|
1352
|
+
|
|
1353
|
+
Returns:
|
|
1354
|
+
Configured OpsGenieAction instance.
|
|
1355
|
+
"""
|
|
1356
|
+
return OpsGenieAction(
|
|
1357
|
+
api_key=api_key,
|
|
1358
|
+
notify_on=notify_on,
|
|
1359
|
+
region=region,
|
|
1360
|
+
priority=priority,
|
|
1361
|
+
auto_priority=auto_priority,
|
|
1362
|
+
template=template,
|
|
1363
|
+
**kwargs,
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def create_critical_alert(
|
|
1368
|
+
api_key: str,
|
|
1369
|
+
*,
|
|
1370
|
+
responders: list[Responder | dict[str, str]] | None = None,
|
|
1371
|
+
tags: list[str] | None = None,
|
|
1372
|
+
region: OpsGenieRegion | str = OpsGenieRegion.US,
|
|
1373
|
+
**kwargs: Any,
|
|
1374
|
+
) -> OpsGenieAction:
|
|
1375
|
+
"""Create an action for critical alerts only.
|
|
1376
|
+
|
|
1377
|
+
This action only triggers on failures and errors with P1 priority.
|
|
1378
|
+
|
|
1379
|
+
Args:
|
|
1380
|
+
api_key: OpsGenie API key.
|
|
1381
|
+
responders: List of responders to notify.
|
|
1382
|
+
tags: Tags to attach to alerts.
|
|
1383
|
+
region: API region.
|
|
1384
|
+
**kwargs: Additional configuration options.
|
|
1385
|
+
|
|
1386
|
+
Returns:
|
|
1387
|
+
Configured OpsGenieAction for critical alerts.
|
|
1388
|
+
"""
|
|
1389
|
+
default_tags = ["truthound", "critical", "data-quality"]
|
|
1390
|
+
return OpsGenieAction(
|
|
1391
|
+
api_key=api_key,
|
|
1392
|
+
notify_on=NotifyCondition.FAILURE_OR_ERROR,
|
|
1393
|
+
region=region,
|
|
1394
|
+
priority=AlertPriority.P1,
|
|
1395
|
+
auto_priority=False,
|
|
1396
|
+
responders=responders or [],
|
|
1397
|
+
tags=tags or default_tags,
|
|
1398
|
+
template="detailed",
|
|
1399
|
+
close_on_success=True,
|
|
1400
|
+
**kwargs,
|
|
1401
|
+
)
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
def create_team_alert(
|
|
1405
|
+
api_key: str,
|
|
1406
|
+
team_name: str,
|
|
1407
|
+
*,
|
|
1408
|
+
notify_on: NotifyCondition | str = NotifyCondition.FAILURE_OR_ERROR,
|
|
1409
|
+
region: OpsGenieRegion | str = OpsGenieRegion.US,
|
|
1410
|
+
auto_priority: bool = True,
|
|
1411
|
+
**kwargs: Any,
|
|
1412
|
+
) -> OpsGenieAction:
|
|
1413
|
+
"""Create an action that alerts a specific team.
|
|
1414
|
+
|
|
1415
|
+
Args:
|
|
1416
|
+
api_key: OpsGenie API key.
|
|
1417
|
+
team_name: Name of the team to notify.
|
|
1418
|
+
notify_on: When to send notifications.
|
|
1419
|
+
region: API region.
|
|
1420
|
+
auto_priority: Auto-map severity to priority.
|
|
1421
|
+
**kwargs: Additional configuration options.
|
|
1422
|
+
|
|
1423
|
+
Returns:
|
|
1424
|
+
Configured OpsGenieAction for team alerts.
|
|
1425
|
+
"""
|
|
1426
|
+
return OpsGenieAction(
|
|
1427
|
+
api_key=api_key,
|
|
1428
|
+
notify_on=notify_on,
|
|
1429
|
+
region=region,
|
|
1430
|
+
auto_priority=auto_priority,
|
|
1431
|
+
responders=[Responder.team(team_name)],
|
|
1432
|
+
**kwargs,
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def create_escalation_alert(
|
|
1437
|
+
api_key: str,
|
|
1438
|
+
escalation_name: str,
|
|
1439
|
+
*,
|
|
1440
|
+
region: OpsGenieRegion | str = OpsGenieRegion.US,
|
|
1441
|
+
**kwargs: Any,
|
|
1442
|
+
) -> OpsGenieAction:
|
|
1443
|
+
"""Create an action that uses an escalation policy.
|
|
1444
|
+
|
|
1445
|
+
Args:
|
|
1446
|
+
api_key: OpsGenie API key.
|
|
1447
|
+
escalation_name: Name of the escalation policy.
|
|
1448
|
+
region: API region.
|
|
1449
|
+
**kwargs: Additional configuration options.
|
|
1450
|
+
|
|
1451
|
+
Returns:
|
|
1452
|
+
Configured OpsGenieAction with escalation.
|
|
1453
|
+
"""
|
|
1454
|
+
return OpsGenieAction(
|
|
1455
|
+
api_key=api_key,
|
|
1456
|
+
notify_on=NotifyCondition.FAILURE_OR_ERROR,
|
|
1457
|
+
region=region,
|
|
1458
|
+
priority=AlertPriority.P1,
|
|
1459
|
+
auto_priority=False,
|
|
1460
|
+
responders=[Responder.escalation(escalation_name)],
|
|
1461
|
+
template="detailed",
|
|
1462
|
+
close_on_success=True,
|
|
1463
|
+
**kwargs,
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
# =============================================================================
|
|
1468
|
+
# Exports
|
|
1469
|
+
# =============================================================================
|
|
1470
|
+
|
|
1471
|
+
__all__ = [
|
|
1472
|
+
# Enums
|
|
1473
|
+
"OpsGenieRegion",
|
|
1474
|
+
"AlertPriority",
|
|
1475
|
+
"AlertAction",
|
|
1476
|
+
"ResponderType",
|
|
1477
|
+
# Responder
|
|
1478
|
+
"Responder",
|
|
1479
|
+
# Builder
|
|
1480
|
+
"AlertPayloadBuilder",
|
|
1481
|
+
# Templates
|
|
1482
|
+
"AlertTemplate",
|
|
1483
|
+
"DefaultAlertTemplate",
|
|
1484
|
+
"MinimalAlertTemplate",
|
|
1485
|
+
"DetailedAlertTemplate",
|
|
1486
|
+
"register_template",
|
|
1487
|
+
"get_template",
|
|
1488
|
+
# HTTP
|
|
1489
|
+
"OpsGenieHTTPClient",
|
|
1490
|
+
"OpsGenieAPIError",
|
|
1491
|
+
# Config & Action
|
|
1492
|
+
"OpsGenieConfig",
|
|
1493
|
+
"OpsGenieAction",
|
|
1494
|
+
# Factory functions
|
|
1495
|
+
"create_opsgenie_action",
|
|
1496
|
+
"create_critical_alert",
|
|
1497
|
+
"create_team_alert",
|
|
1498
|
+
"create_escalation_alert",
|
|
1499
|
+
]
|