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,1570 @@
|
|
|
1
|
+
"""Microsoft Teams notification action.
|
|
2
|
+
|
|
3
|
+
This module provides Microsoft Teams integration for checkpoint notifications
|
|
4
|
+
using Incoming Webhooks and Adaptive Cards.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Adaptive Card formatting for rich message display
|
|
8
|
+
- Multiple connector types (Incoming Webhook, Power Automate)
|
|
9
|
+
- Customizable message templates
|
|
10
|
+
- Action buttons for quick navigation
|
|
11
|
+
- @mention support for users and channels
|
|
12
|
+
- Thread/reply support for conversation continuity
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from truthound.checkpoint.actions import TeamsNotification
|
|
16
|
+
>>>
|
|
17
|
+
>>> action = TeamsNotification(
|
|
18
|
+
... webhook_url="https://outlook.office.com/webhook/...",
|
|
19
|
+
... notify_on="failure",
|
|
20
|
+
... include_actions=True,
|
|
21
|
+
... mention_on_failure=["user@example.com"],
|
|
22
|
+
... )
|
|
23
|
+
>>> result = action.execute(checkpoint_result)
|
|
24
|
+
|
|
25
|
+
References:
|
|
26
|
+
- Adaptive Cards: https://adaptivecards.io/
|
|
27
|
+
- Teams Webhooks: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
from abc import ABC, abstractmethod
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
from enum import Enum
|
|
38
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
39
|
+
|
|
40
|
+
from truthound.checkpoint.actions.base import (
|
|
41
|
+
ActionConfig,
|
|
42
|
+
ActionResult,
|
|
43
|
+
ActionStatus,
|
|
44
|
+
BaseAction,
|
|
45
|
+
NotifyCondition,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from truthound.checkpoint.checkpoint import CheckpointResult
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Enums and Constants
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TeamsConnectorType(str, Enum):
|
|
58
|
+
"""Types of Teams connectors supported."""
|
|
59
|
+
|
|
60
|
+
INCOMING_WEBHOOK = "incoming_webhook"
|
|
61
|
+
POWER_AUTOMATE = "power_automate"
|
|
62
|
+
LOGIC_APPS = "logic_apps"
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return self.value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AdaptiveCardVersion(str, Enum):
|
|
69
|
+
"""Adaptive Card schema versions."""
|
|
70
|
+
|
|
71
|
+
V1_0 = "1.0"
|
|
72
|
+
V1_2 = "1.2"
|
|
73
|
+
V1_3 = "1.3"
|
|
74
|
+
V1_4 = "1.4"
|
|
75
|
+
V1_5 = "1.5"
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
return self.value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MessageTheme(str, Enum):
|
|
82
|
+
"""Pre-defined message themes."""
|
|
83
|
+
|
|
84
|
+
DEFAULT = "default"
|
|
85
|
+
MINIMAL = "minimal"
|
|
86
|
+
DETAILED = "detailed"
|
|
87
|
+
COMPACT = "compact"
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return self.value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CardContainerStyle(str, Enum):
|
|
94
|
+
"""Container styles for Adaptive Cards."""
|
|
95
|
+
|
|
96
|
+
DEFAULT = "default"
|
|
97
|
+
EMPHASIS = "emphasis"
|
|
98
|
+
GOOD = "good"
|
|
99
|
+
ATTENTION = "attention"
|
|
100
|
+
WARNING = "warning"
|
|
101
|
+
ACCENT = "accent"
|
|
102
|
+
|
|
103
|
+
def __str__(self) -> str:
|
|
104
|
+
return self.value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TextWeight(str, Enum):
|
|
108
|
+
"""Text weight options."""
|
|
109
|
+
|
|
110
|
+
DEFAULT = "default"
|
|
111
|
+
LIGHTER = "lighter"
|
|
112
|
+
BOLDER = "bolder"
|
|
113
|
+
|
|
114
|
+
def __str__(self) -> str:
|
|
115
|
+
return self.value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TextSize(str, Enum):
|
|
119
|
+
"""Text size options."""
|
|
120
|
+
|
|
121
|
+
DEFAULT = "default"
|
|
122
|
+
SMALL = "small"
|
|
123
|
+
MEDIUM = "medium"
|
|
124
|
+
LARGE = "large"
|
|
125
|
+
EXTRA_LARGE = "extraLarge"
|
|
126
|
+
|
|
127
|
+
def __str__(self) -> str:
|
|
128
|
+
return self.value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TextColor(str, Enum):
|
|
132
|
+
"""Text color options."""
|
|
133
|
+
|
|
134
|
+
DEFAULT = "default"
|
|
135
|
+
DARK = "dark"
|
|
136
|
+
LIGHT = "light"
|
|
137
|
+
ACCENT = "accent"
|
|
138
|
+
GOOD = "good"
|
|
139
|
+
WARNING = "warning"
|
|
140
|
+
ATTENTION = "attention"
|
|
141
|
+
|
|
142
|
+
def __str__(self) -> str:
|
|
143
|
+
return self.value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# Adaptive Card Builder (Builder Pattern)
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@runtime_checkable
|
|
152
|
+
class CardElement(Protocol):
|
|
153
|
+
"""Protocol for Adaptive Card elements."""
|
|
154
|
+
|
|
155
|
+
def to_dict(self) -> dict[str, Any]:
|
|
156
|
+
"""Convert element to dictionary representation."""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class AdaptiveCardBuilder:
|
|
161
|
+
"""Fluent builder for creating Adaptive Cards.
|
|
162
|
+
|
|
163
|
+
This builder provides a type-safe, fluent API for constructing
|
|
164
|
+
Microsoft Adaptive Cards with proper schema validation.
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> card = (
|
|
168
|
+
... AdaptiveCardBuilder()
|
|
169
|
+
... .set_version("1.4")
|
|
170
|
+
... .add_text_block("Hello World", weight="bolder", size="large")
|
|
171
|
+
... .add_fact_set([("Name", "John"), ("Status", "Active")])
|
|
172
|
+
... .add_action_open_url("View Details", "https://example.com")
|
|
173
|
+
... .build()
|
|
174
|
+
... )
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, version: str | AdaptiveCardVersion = AdaptiveCardVersion.V1_4) -> None:
|
|
178
|
+
"""Initialize the builder.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
version: Adaptive Card schema version to use.
|
|
182
|
+
"""
|
|
183
|
+
self._version = str(version)
|
|
184
|
+
self._body: list[dict[str, Any]] = []
|
|
185
|
+
self._actions: list[dict[str, Any]] = []
|
|
186
|
+
self._fallback_text: str | None = None
|
|
187
|
+
self._speak: str | None = None
|
|
188
|
+
self._ms_teams: dict[str, Any] = {}
|
|
189
|
+
self._min_height: str | None = None
|
|
190
|
+
self._vertical_content_alignment: str | None = None
|
|
191
|
+
|
|
192
|
+
def set_version(self, version: str | AdaptiveCardVersion) -> "AdaptiveCardBuilder":
|
|
193
|
+
"""Set the Adaptive Card schema version.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
version: Schema version string (e.g., "1.4").
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Self for method chaining.
|
|
200
|
+
"""
|
|
201
|
+
self._version = str(version)
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def set_fallback_text(self, text: str) -> "AdaptiveCardBuilder":
|
|
205
|
+
"""Set fallback text for clients that don't support Adaptive Cards.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
text: Plain text fallback.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Self for method chaining.
|
|
212
|
+
"""
|
|
213
|
+
self._fallback_text = text
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
def set_speak(self, speak: str) -> "AdaptiveCardBuilder":
|
|
217
|
+
"""Set speech text for accessibility.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
speak: SSML or plain text for speech.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Self for method chaining.
|
|
224
|
+
"""
|
|
225
|
+
self._speak = speak
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def set_min_height(self, height: str) -> "AdaptiveCardBuilder":
|
|
229
|
+
"""Set minimum height of the card.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
height: CSS height value (e.g., "200px").
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Self for method chaining.
|
|
236
|
+
"""
|
|
237
|
+
self._min_height = height
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def enable_full_width(self) -> "AdaptiveCardBuilder":
|
|
241
|
+
"""Enable full-width display in Teams.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Self for method chaining.
|
|
245
|
+
"""
|
|
246
|
+
self._ms_teams["width"] = "Full"
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
# -------------------------------------------------------------------------
|
|
250
|
+
# Text Elements
|
|
251
|
+
# -------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def add_text_block(
|
|
254
|
+
self,
|
|
255
|
+
text: str,
|
|
256
|
+
*,
|
|
257
|
+
weight: str | TextWeight = TextWeight.DEFAULT,
|
|
258
|
+
size: str | TextSize = TextSize.DEFAULT,
|
|
259
|
+
color: str | TextColor = TextColor.DEFAULT,
|
|
260
|
+
wrap: bool = True,
|
|
261
|
+
is_subtle: bool = False,
|
|
262
|
+
max_lines: int | None = None,
|
|
263
|
+
horizontal_alignment: str = "left",
|
|
264
|
+
spacing: str = "default",
|
|
265
|
+
separator: bool = False,
|
|
266
|
+
) -> "AdaptiveCardBuilder":
|
|
267
|
+
"""Add a text block element.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
text: Text content (supports Markdown in some contexts).
|
|
271
|
+
weight: Font weight.
|
|
272
|
+
size: Font size.
|
|
273
|
+
color: Text color.
|
|
274
|
+
wrap: Whether text should wrap.
|
|
275
|
+
is_subtle: Whether to display as subtle/muted.
|
|
276
|
+
max_lines: Maximum number of lines to display.
|
|
277
|
+
horizontal_alignment: Text alignment (left, center, right).
|
|
278
|
+
spacing: Spacing above this element.
|
|
279
|
+
separator: Whether to show a separator line above.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Self for method chaining.
|
|
283
|
+
"""
|
|
284
|
+
element: dict[str, Any] = {
|
|
285
|
+
"type": "TextBlock",
|
|
286
|
+
"text": text,
|
|
287
|
+
"wrap": wrap,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if str(weight) != "default":
|
|
291
|
+
element["weight"] = str(weight)
|
|
292
|
+
if str(size) != "default":
|
|
293
|
+
element["size"] = str(size)
|
|
294
|
+
if str(color) != "default":
|
|
295
|
+
element["color"] = str(color)
|
|
296
|
+
if is_subtle:
|
|
297
|
+
element["isSubtle"] = True
|
|
298
|
+
if max_lines is not None:
|
|
299
|
+
element["maxLines"] = max_lines
|
|
300
|
+
if horizontal_alignment != "left":
|
|
301
|
+
element["horizontalAlignment"] = horizontal_alignment
|
|
302
|
+
if spacing != "default":
|
|
303
|
+
element["spacing"] = spacing
|
|
304
|
+
if separator:
|
|
305
|
+
element["separator"] = True
|
|
306
|
+
|
|
307
|
+
self._body.append(element)
|
|
308
|
+
return self
|
|
309
|
+
|
|
310
|
+
def add_rich_text_block(
|
|
311
|
+
self,
|
|
312
|
+
inlines: list[dict[str, Any]],
|
|
313
|
+
*,
|
|
314
|
+
horizontal_alignment: str = "left",
|
|
315
|
+
spacing: str = "default",
|
|
316
|
+
) -> "AdaptiveCardBuilder":
|
|
317
|
+
"""Add a rich text block with inline elements.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
inlines: List of inline text elements.
|
|
321
|
+
horizontal_alignment: Text alignment.
|
|
322
|
+
spacing: Spacing above this element.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Self for method chaining.
|
|
326
|
+
"""
|
|
327
|
+
element: dict[str, Any] = {
|
|
328
|
+
"type": "RichTextBlock",
|
|
329
|
+
"inlines": inlines,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if horizontal_alignment != "left":
|
|
333
|
+
element["horizontalAlignment"] = horizontal_alignment
|
|
334
|
+
if spacing != "default":
|
|
335
|
+
element["spacing"] = spacing
|
|
336
|
+
|
|
337
|
+
self._body.append(element)
|
|
338
|
+
return self
|
|
339
|
+
|
|
340
|
+
# -------------------------------------------------------------------------
|
|
341
|
+
# Container Elements
|
|
342
|
+
# -------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
def add_container(
|
|
345
|
+
self,
|
|
346
|
+
items: list[dict[str, Any]],
|
|
347
|
+
*,
|
|
348
|
+
style: str | CardContainerStyle = CardContainerStyle.DEFAULT,
|
|
349
|
+
bleed: bool = False,
|
|
350
|
+
min_height: str | None = None,
|
|
351
|
+
spacing: str = "default",
|
|
352
|
+
separator: bool = False,
|
|
353
|
+
) -> "AdaptiveCardBuilder":
|
|
354
|
+
"""Add a container element.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
items: Child elements in the container.
|
|
358
|
+
style: Container style.
|
|
359
|
+
bleed: Whether to bleed to card edges.
|
|
360
|
+
min_height: Minimum container height.
|
|
361
|
+
spacing: Spacing above this element.
|
|
362
|
+
separator: Whether to show a separator.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Self for method chaining.
|
|
366
|
+
"""
|
|
367
|
+
element: dict[str, Any] = {
|
|
368
|
+
"type": "Container",
|
|
369
|
+
"items": items,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if str(style) != "default":
|
|
373
|
+
element["style"] = str(style)
|
|
374
|
+
if bleed:
|
|
375
|
+
element["bleed"] = True
|
|
376
|
+
if min_height:
|
|
377
|
+
element["minHeight"] = min_height
|
|
378
|
+
if spacing != "default":
|
|
379
|
+
element["spacing"] = spacing
|
|
380
|
+
if separator:
|
|
381
|
+
element["separator"] = True
|
|
382
|
+
|
|
383
|
+
self._body.append(element)
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def add_column_set(
|
|
387
|
+
self,
|
|
388
|
+
columns: list[dict[str, Any]],
|
|
389
|
+
*,
|
|
390
|
+
spacing: str = "default",
|
|
391
|
+
separator: bool = False,
|
|
392
|
+
) -> "AdaptiveCardBuilder":
|
|
393
|
+
"""Add a column set for horizontal layout.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
columns: List of column definitions.
|
|
397
|
+
spacing: Spacing above this element.
|
|
398
|
+
separator: Whether to show a separator.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Self for method chaining.
|
|
402
|
+
"""
|
|
403
|
+
element: dict[str, Any] = {
|
|
404
|
+
"type": "ColumnSet",
|
|
405
|
+
"columns": columns,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if spacing != "default":
|
|
409
|
+
element["spacing"] = spacing
|
|
410
|
+
if separator:
|
|
411
|
+
element["separator"] = True
|
|
412
|
+
|
|
413
|
+
self._body.append(element)
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
# -------------------------------------------------------------------------
|
|
417
|
+
# Data Display Elements
|
|
418
|
+
# -------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
def add_fact_set(
|
|
421
|
+
self,
|
|
422
|
+
facts: list[tuple[str, str]],
|
|
423
|
+
*,
|
|
424
|
+
spacing: str = "default",
|
|
425
|
+
separator: bool = False,
|
|
426
|
+
) -> "AdaptiveCardBuilder":
|
|
427
|
+
"""Add a fact set for key-value display.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
facts: List of (title, value) tuples.
|
|
431
|
+
spacing: Spacing above this element.
|
|
432
|
+
separator: Whether to show a separator.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Self for method chaining.
|
|
436
|
+
"""
|
|
437
|
+
element: dict[str, Any] = {
|
|
438
|
+
"type": "FactSet",
|
|
439
|
+
"facts": [{"title": title, "value": value} for title, value in facts],
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if spacing != "default":
|
|
443
|
+
element["spacing"] = spacing
|
|
444
|
+
if separator:
|
|
445
|
+
element["separator"] = True
|
|
446
|
+
|
|
447
|
+
self._body.append(element)
|
|
448
|
+
return self
|
|
449
|
+
|
|
450
|
+
def add_image(
|
|
451
|
+
self,
|
|
452
|
+
url: str,
|
|
453
|
+
*,
|
|
454
|
+
alt_text: str = "",
|
|
455
|
+
size: str = "auto",
|
|
456
|
+
style: str = "default",
|
|
457
|
+
horizontal_alignment: str = "left",
|
|
458
|
+
spacing: str = "default",
|
|
459
|
+
) -> "AdaptiveCardBuilder":
|
|
460
|
+
"""Add an image element.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
url: Image URL.
|
|
464
|
+
alt_text: Alternative text for accessibility.
|
|
465
|
+
size: Image size (auto, stretch, small, medium, large).
|
|
466
|
+
style: Image style (default, person for circular).
|
|
467
|
+
horizontal_alignment: Image alignment.
|
|
468
|
+
spacing: Spacing above this element.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Self for method chaining.
|
|
472
|
+
"""
|
|
473
|
+
element: dict[str, Any] = {
|
|
474
|
+
"type": "Image",
|
|
475
|
+
"url": url,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if alt_text:
|
|
479
|
+
element["altText"] = alt_text
|
|
480
|
+
if size != "auto":
|
|
481
|
+
element["size"] = size
|
|
482
|
+
if style != "default":
|
|
483
|
+
element["style"] = style
|
|
484
|
+
if horizontal_alignment != "left":
|
|
485
|
+
element["horizontalAlignment"] = horizontal_alignment
|
|
486
|
+
if spacing != "default":
|
|
487
|
+
element["spacing"] = spacing
|
|
488
|
+
|
|
489
|
+
self._body.append(element)
|
|
490
|
+
return self
|
|
491
|
+
|
|
492
|
+
def add_image_set(
|
|
493
|
+
self,
|
|
494
|
+
images: list[dict[str, Any]],
|
|
495
|
+
*,
|
|
496
|
+
image_size: str = "medium",
|
|
497
|
+
spacing: str = "default",
|
|
498
|
+
) -> "AdaptiveCardBuilder":
|
|
499
|
+
"""Add an image set element.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
images: List of image definitions.
|
|
503
|
+
image_size: Size for all images.
|
|
504
|
+
spacing: Spacing above this element.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Self for method chaining.
|
|
508
|
+
"""
|
|
509
|
+
element: dict[str, Any] = {
|
|
510
|
+
"type": "ImageSet",
|
|
511
|
+
"images": images,
|
|
512
|
+
"imageSize": image_size,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if spacing != "default":
|
|
516
|
+
element["spacing"] = spacing
|
|
517
|
+
|
|
518
|
+
self._body.append(element)
|
|
519
|
+
return self
|
|
520
|
+
|
|
521
|
+
# -------------------------------------------------------------------------
|
|
522
|
+
# Action Elements
|
|
523
|
+
# -------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
def add_action_open_url(
|
|
526
|
+
self,
|
|
527
|
+
title: str,
|
|
528
|
+
url: str,
|
|
529
|
+
*,
|
|
530
|
+
icon_url: str | None = None,
|
|
531
|
+
tooltip: str | None = None,
|
|
532
|
+
style: str = "default",
|
|
533
|
+
) -> "AdaptiveCardBuilder":
|
|
534
|
+
"""Add an action that opens a URL.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
title: Button text.
|
|
538
|
+
url: URL to open.
|
|
539
|
+
icon_url: Optional icon URL.
|
|
540
|
+
tooltip: Hover tooltip text.
|
|
541
|
+
style: Button style (default, positive, destructive).
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Self for method chaining.
|
|
545
|
+
"""
|
|
546
|
+
action: dict[str, Any] = {
|
|
547
|
+
"type": "Action.OpenUrl",
|
|
548
|
+
"title": title,
|
|
549
|
+
"url": url,
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if icon_url:
|
|
553
|
+
action["iconUrl"] = icon_url
|
|
554
|
+
if tooltip:
|
|
555
|
+
action["tooltip"] = tooltip
|
|
556
|
+
if style != "default":
|
|
557
|
+
action["style"] = style
|
|
558
|
+
|
|
559
|
+
self._actions.append(action)
|
|
560
|
+
return self
|
|
561
|
+
|
|
562
|
+
def add_action_submit(
|
|
563
|
+
self,
|
|
564
|
+
title: str,
|
|
565
|
+
data: dict[str, Any] | None = None,
|
|
566
|
+
*,
|
|
567
|
+
style: str = "default",
|
|
568
|
+
) -> "AdaptiveCardBuilder":
|
|
569
|
+
"""Add an action that submits data.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
title: Button text.
|
|
573
|
+
data: Data to submit.
|
|
574
|
+
style: Button style.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Self for method chaining.
|
|
578
|
+
"""
|
|
579
|
+
action: dict[str, Any] = {
|
|
580
|
+
"type": "Action.Submit",
|
|
581
|
+
"title": title,
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if data:
|
|
585
|
+
action["data"] = data
|
|
586
|
+
if style != "default":
|
|
587
|
+
action["style"] = style
|
|
588
|
+
|
|
589
|
+
self._actions.append(action)
|
|
590
|
+
return self
|
|
591
|
+
|
|
592
|
+
def add_action_show_card(
|
|
593
|
+
self,
|
|
594
|
+
title: str,
|
|
595
|
+
card: dict[str, Any],
|
|
596
|
+
*,
|
|
597
|
+
style: str = "default",
|
|
598
|
+
) -> "AdaptiveCardBuilder":
|
|
599
|
+
"""Add an action that shows a nested card.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
title: Button text.
|
|
603
|
+
card: Nested Adaptive Card definition.
|
|
604
|
+
style: Button style.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
Self for method chaining.
|
|
608
|
+
"""
|
|
609
|
+
action: dict[str, Any] = {
|
|
610
|
+
"type": "Action.ShowCard",
|
|
611
|
+
"title": title,
|
|
612
|
+
"card": card,
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if style != "default":
|
|
616
|
+
action["style"] = style
|
|
617
|
+
|
|
618
|
+
self._actions.append(action)
|
|
619
|
+
return self
|
|
620
|
+
|
|
621
|
+
def add_action_toggle_visibility(
|
|
622
|
+
self,
|
|
623
|
+
title: str,
|
|
624
|
+
target_elements: list[str | dict[str, Any]],
|
|
625
|
+
*,
|
|
626
|
+
style: str = "default",
|
|
627
|
+
) -> "AdaptiveCardBuilder":
|
|
628
|
+
"""Add an action that toggles element visibility.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
title: Button text.
|
|
632
|
+
target_elements: IDs or targets to toggle.
|
|
633
|
+
style: Button style.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Self for method chaining.
|
|
637
|
+
"""
|
|
638
|
+
action: dict[str, Any] = {
|
|
639
|
+
"type": "Action.ToggleVisibility",
|
|
640
|
+
"title": title,
|
|
641
|
+
"targetElements": target_elements,
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if style != "default":
|
|
645
|
+
action["style"] = style
|
|
646
|
+
|
|
647
|
+
self._actions.append(action)
|
|
648
|
+
return self
|
|
649
|
+
|
|
650
|
+
# -------------------------------------------------------------------------
|
|
651
|
+
# Raw Element Addition
|
|
652
|
+
# -------------------------------------------------------------------------
|
|
653
|
+
|
|
654
|
+
def add_element(self, element: dict[str, Any]) -> "AdaptiveCardBuilder":
|
|
655
|
+
"""Add a raw element to the card body.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
element: Element definition dictionary.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Self for method chaining.
|
|
662
|
+
"""
|
|
663
|
+
self._body.append(element)
|
|
664
|
+
return self
|
|
665
|
+
|
|
666
|
+
def add_action(self, action: dict[str, Any]) -> "AdaptiveCardBuilder":
|
|
667
|
+
"""Add a raw action to the card.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
action: Action definition dictionary.
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Self for method chaining.
|
|
674
|
+
"""
|
|
675
|
+
self._actions.append(action)
|
|
676
|
+
return self
|
|
677
|
+
|
|
678
|
+
# -------------------------------------------------------------------------
|
|
679
|
+
# Build
|
|
680
|
+
# -------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
def build(self) -> dict[str, Any]:
|
|
683
|
+
"""Build the final Adaptive Card.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
Complete Adaptive Card as a dictionary.
|
|
687
|
+
"""
|
|
688
|
+
card: dict[str, Any] = {
|
|
689
|
+
"type": "AdaptiveCard",
|
|
690
|
+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
691
|
+
"version": self._version,
|
|
692
|
+
"body": self._body,
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if self._actions:
|
|
696
|
+
card["actions"] = self._actions
|
|
697
|
+
|
|
698
|
+
if self._fallback_text:
|
|
699
|
+
card["fallbackText"] = self._fallback_text
|
|
700
|
+
|
|
701
|
+
if self._speak:
|
|
702
|
+
card["speak"] = self._speak
|
|
703
|
+
|
|
704
|
+
if self._min_height:
|
|
705
|
+
card["minHeight"] = self._min_height
|
|
706
|
+
|
|
707
|
+
if self._vertical_content_alignment:
|
|
708
|
+
card["verticalContentAlignment"] = self._vertical_content_alignment
|
|
709
|
+
|
|
710
|
+
if self._ms_teams:
|
|
711
|
+
card["msteams"] = self._ms_teams
|
|
712
|
+
|
|
713
|
+
return card
|
|
714
|
+
|
|
715
|
+
def build_message_card(self) -> dict[str, Any]:
|
|
716
|
+
"""Build a complete Teams message containing the Adaptive Card.
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
Teams message payload with the Adaptive Card.
|
|
720
|
+
"""
|
|
721
|
+
return {
|
|
722
|
+
"type": "message",
|
|
723
|
+
"attachments": [
|
|
724
|
+
{
|
|
725
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
726
|
+
"contentUrl": None,
|
|
727
|
+
"content": self.build(),
|
|
728
|
+
}
|
|
729
|
+
],
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# =============================================================================
|
|
734
|
+
# Message Template System
|
|
735
|
+
# =============================================================================
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class MessageTemplate(ABC):
|
|
739
|
+
"""Abstract base class for message templates."""
|
|
740
|
+
|
|
741
|
+
@abstractmethod
|
|
742
|
+
def render(
|
|
743
|
+
self,
|
|
744
|
+
checkpoint_result: "CheckpointResult",
|
|
745
|
+
config: "TeamsConfig",
|
|
746
|
+
) -> dict[str, Any]:
|
|
747
|
+
"""Render the template with the given data.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
checkpoint_result: Validation result data.
|
|
751
|
+
config: Teams notification configuration.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Complete Teams message payload.
|
|
755
|
+
"""
|
|
756
|
+
pass
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class DefaultTemplate(MessageTemplate):
|
|
760
|
+
"""Default message template with full details."""
|
|
761
|
+
|
|
762
|
+
# Status configuration
|
|
763
|
+
STATUS_CONFIG: dict[str, dict[str, str]] = {
|
|
764
|
+
"success": {
|
|
765
|
+
"color": "good",
|
|
766
|
+
"emoji": "✅",
|
|
767
|
+
"title": "Validation Passed",
|
|
768
|
+
"accent_color": "#28a745",
|
|
769
|
+
},
|
|
770
|
+
"failure": {
|
|
771
|
+
"color": "attention",
|
|
772
|
+
"emoji": "❌",
|
|
773
|
+
"title": "Validation Failed",
|
|
774
|
+
"accent_color": "#dc3545",
|
|
775
|
+
},
|
|
776
|
+
"error": {
|
|
777
|
+
"color": "attention",
|
|
778
|
+
"emoji": "⚠️",
|
|
779
|
+
"title": "Validation Error",
|
|
780
|
+
"accent_color": "#dc3545",
|
|
781
|
+
},
|
|
782
|
+
"warning": {
|
|
783
|
+
"color": "warning",
|
|
784
|
+
"emoji": "⚠️",
|
|
785
|
+
"title": "Validation Warning",
|
|
786
|
+
"accent_color": "#ffc107",
|
|
787
|
+
},
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
def render(
|
|
791
|
+
self,
|
|
792
|
+
checkpoint_result: "CheckpointResult",
|
|
793
|
+
config: "TeamsConfig",
|
|
794
|
+
) -> dict[str, Any]:
|
|
795
|
+
"""Render the default template."""
|
|
796
|
+
status = checkpoint_result.status.value
|
|
797
|
+
status_config = self.STATUS_CONFIG.get(
|
|
798
|
+
status, {"color": "default", "emoji": "❓", "title": "Unknown", "accent_color": "#6c757d"}
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
validation = checkpoint_result.validation_result
|
|
802
|
+
stats = validation.statistics if validation else None
|
|
803
|
+
|
|
804
|
+
builder = AdaptiveCardBuilder(config.card_version)
|
|
805
|
+
|
|
806
|
+
if config.full_width:
|
|
807
|
+
builder.enable_full_width()
|
|
808
|
+
|
|
809
|
+
# Build mentions string
|
|
810
|
+
mentions_text = ""
|
|
811
|
+
mentions_entities: list[dict[str, Any]] = []
|
|
812
|
+
if status in ("failure", "error") and config.mention_on_failure:
|
|
813
|
+
for mention in config.mention_on_failure:
|
|
814
|
+
mention_id = mention.get("id", mention) if isinstance(mention, dict) else mention
|
|
815
|
+
mention_name = mention.get("name", mention_id) if isinstance(mention, dict) else mention_id
|
|
816
|
+
mentions_text += f"<at>{mention_name}</at> "
|
|
817
|
+
mentions_entities.append({
|
|
818
|
+
"type": "mention",
|
|
819
|
+
"text": f"<at>{mention_name}</at>",
|
|
820
|
+
"mentioned": {
|
|
821
|
+
"id": mention_id,
|
|
822
|
+
"name": mention_name,
|
|
823
|
+
},
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
# Header with status
|
|
827
|
+
header_text = f"{status_config['emoji']} **{status_config['title']}**"
|
|
828
|
+
if mentions_text:
|
|
829
|
+
header_text = f"{mentions_text.strip()} {header_text}"
|
|
830
|
+
|
|
831
|
+
builder.add_text_block(
|
|
832
|
+
header_text,
|
|
833
|
+
weight=TextWeight.BOLDER,
|
|
834
|
+
size=TextSize.LARGE,
|
|
835
|
+
color=TextColor.ATTENTION if status in ("failure", "error") else TextColor.DEFAULT,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# Checkpoint name
|
|
839
|
+
builder.add_text_block(
|
|
840
|
+
f"Checkpoint: **{checkpoint_result.checkpoint_name}**",
|
|
841
|
+
size=TextSize.MEDIUM,
|
|
842
|
+
spacing="small",
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# Facts section
|
|
846
|
+
facts: list[tuple[str, str]] = [
|
|
847
|
+
("Data Asset", checkpoint_result.data_asset or "N/A"),
|
|
848
|
+
("Run ID", checkpoint_result.run_id[:12] + "..." if len(checkpoint_result.run_id) > 12 else checkpoint_result.run_id),
|
|
849
|
+
("Run Time", checkpoint_result.run_time.strftime("%Y-%m-%d %H:%M:%S")),
|
|
850
|
+
("Duration", f"{checkpoint_result.duration_ms:.1f}ms"),
|
|
851
|
+
]
|
|
852
|
+
|
|
853
|
+
if stats:
|
|
854
|
+
facts.extend([
|
|
855
|
+
("Total Issues", str(stats.total_issues)),
|
|
856
|
+
("Pass Rate", f"{stats.pass_rate * 100:.1f}%"),
|
|
857
|
+
])
|
|
858
|
+
|
|
859
|
+
builder.add_fact_set(facts, spacing="medium", separator=True)
|
|
860
|
+
|
|
861
|
+
# Issue breakdown if there are issues
|
|
862
|
+
if stats and stats.total_issues > 0 and config.include_details:
|
|
863
|
+
breakdown_text = (
|
|
864
|
+
f"🔴 Critical: {stats.critical_issues} | "
|
|
865
|
+
f"🟠 High: {stats.high_issues} | "
|
|
866
|
+
f"🟡 Medium: {stats.medium_issues} | "
|
|
867
|
+
f"🔵 Low: {stats.low_issues}"
|
|
868
|
+
)
|
|
869
|
+
builder.add_text_block(
|
|
870
|
+
breakdown_text,
|
|
871
|
+
size=TextSize.SMALL,
|
|
872
|
+
is_subtle=True,
|
|
873
|
+
spacing="small",
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
# Action buttons
|
|
877
|
+
if config.include_actions:
|
|
878
|
+
if config.dashboard_url:
|
|
879
|
+
builder.add_action_open_url(
|
|
880
|
+
"View Dashboard",
|
|
881
|
+
config.dashboard_url.format(
|
|
882
|
+
run_id=checkpoint_result.run_id,
|
|
883
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
884
|
+
),
|
|
885
|
+
style="positive",
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
if config.details_url:
|
|
889
|
+
builder.add_action_open_url(
|
|
890
|
+
"View Details",
|
|
891
|
+
config.details_url.format(
|
|
892
|
+
run_id=checkpoint_result.run_id,
|
|
893
|
+
checkpoint=checkpoint_result.checkpoint_name,
|
|
894
|
+
),
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
# Build the card
|
|
898
|
+
card = builder.build()
|
|
899
|
+
|
|
900
|
+
# Add mentions to msteams section if any
|
|
901
|
+
if mentions_entities:
|
|
902
|
+
card.setdefault("msteams", {})["entities"] = mentions_entities
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
"type": "message",
|
|
906
|
+
"attachments": [
|
|
907
|
+
{
|
|
908
|
+
"contentType": "application/vnd.microsoft.card.adaptive",
|
|
909
|
+
"contentUrl": None,
|
|
910
|
+
"content": card,
|
|
911
|
+
}
|
|
912
|
+
],
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
class MinimalTemplate(MessageTemplate):
|
|
917
|
+
"""Minimal template with just status and key info."""
|
|
918
|
+
|
|
919
|
+
def render(
|
|
920
|
+
self,
|
|
921
|
+
checkpoint_result: "CheckpointResult",
|
|
922
|
+
config: "TeamsConfig",
|
|
923
|
+
) -> dict[str, Any]:
|
|
924
|
+
"""Render minimal template."""
|
|
925
|
+
status = checkpoint_result.status.value
|
|
926
|
+
validation = checkpoint_result.validation_result
|
|
927
|
+
stats = validation.statistics if validation else None
|
|
928
|
+
|
|
929
|
+
status_emoji = {"success": "✅", "failure": "❌", "error": "⚠️", "warning": "⚠️"}.get(status, "❓")
|
|
930
|
+
issues_text = f" - {stats.total_issues} issues" if stats and stats.total_issues > 0 else ""
|
|
931
|
+
|
|
932
|
+
builder = AdaptiveCardBuilder(config.card_version)
|
|
933
|
+
builder.add_text_block(
|
|
934
|
+
f"{status_emoji} **{checkpoint_result.checkpoint_name}** ({status.upper()}){issues_text}",
|
|
935
|
+
wrap=True,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
if stats:
|
|
939
|
+
builder.add_text_block(
|
|
940
|
+
f"Pass rate: {stats.pass_rate * 100:.1f}%",
|
|
941
|
+
size=TextSize.SMALL,
|
|
942
|
+
is_subtle=True,
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
return builder.build_message_card()
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class DetailedTemplate(MessageTemplate):
|
|
949
|
+
"""Detailed template with expandable sections."""
|
|
950
|
+
|
|
951
|
+
def render(
|
|
952
|
+
self,
|
|
953
|
+
checkpoint_result: "CheckpointResult",
|
|
954
|
+
config: "TeamsConfig",
|
|
955
|
+
) -> dict[str, Any]:
|
|
956
|
+
"""Render detailed template with expandable sections."""
|
|
957
|
+
default = DefaultTemplate()
|
|
958
|
+
base_message = default.render(checkpoint_result, config)
|
|
959
|
+
|
|
960
|
+
# Add additional details card for show/hide
|
|
961
|
+
validation = checkpoint_result.validation_result
|
|
962
|
+
if validation and validation.results and config.include_details:
|
|
963
|
+
# Build details card
|
|
964
|
+
details_builder = AdaptiveCardBuilder()
|
|
965
|
+
|
|
966
|
+
# Show first N issue details
|
|
967
|
+
max_issues = min(5, len(validation.results))
|
|
968
|
+
for i, result in enumerate(validation.results[:max_issues]):
|
|
969
|
+
issue_text = f"**{result.validator_name}** on `{result.column or 'N/A'}`"
|
|
970
|
+
if hasattr(result, "message"):
|
|
971
|
+
issue_text += f": {result.message}"
|
|
972
|
+
|
|
973
|
+
details_builder.add_text_block(
|
|
974
|
+
issue_text,
|
|
975
|
+
size=TextSize.SMALL,
|
|
976
|
+
wrap=True,
|
|
977
|
+
spacing="small" if i > 0 else "none",
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
if len(validation.results) > max_issues:
|
|
981
|
+
details_builder.add_text_block(
|
|
982
|
+
f"... and {len(validation.results) - max_issues} more issues",
|
|
983
|
+
size=TextSize.SMALL,
|
|
984
|
+
is_subtle=True,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
# Add show card action to main card
|
|
988
|
+
content = base_message["attachments"][0]["content"]
|
|
989
|
+
content.setdefault("actions", []).append({
|
|
990
|
+
"type": "Action.ShowCard",
|
|
991
|
+
"title": "Show Issue Details",
|
|
992
|
+
"card": details_builder.build(),
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
return base_message
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
class CompactTemplate(MessageTemplate):
|
|
999
|
+
"""Compact template for high-volume notifications."""
|
|
1000
|
+
|
|
1001
|
+
def render(
|
|
1002
|
+
self,
|
|
1003
|
+
checkpoint_result: "CheckpointResult",
|
|
1004
|
+
config: "TeamsConfig",
|
|
1005
|
+
) -> dict[str, Any]:
|
|
1006
|
+
"""Render compact template."""
|
|
1007
|
+
status = checkpoint_result.status.value
|
|
1008
|
+
validation = checkpoint_result.validation_result
|
|
1009
|
+
stats = validation.statistics if validation else None
|
|
1010
|
+
|
|
1011
|
+
status_emoji = {"success": "✅", "failure": "❌", "error": "⚠️", "warning": "⚠️"}.get(status, "❓")
|
|
1012
|
+
|
|
1013
|
+
columns = [
|
|
1014
|
+
{
|
|
1015
|
+
"type": "Column",
|
|
1016
|
+
"width": "auto",
|
|
1017
|
+
"items": [
|
|
1018
|
+
{"type": "TextBlock", "text": status_emoji, "size": "large"},
|
|
1019
|
+
],
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
"type": "Column",
|
|
1023
|
+
"width": "stretch",
|
|
1024
|
+
"items": [
|
|
1025
|
+
{
|
|
1026
|
+
"type": "TextBlock",
|
|
1027
|
+
"text": f"**{checkpoint_result.checkpoint_name}**",
|
|
1028
|
+
"weight": "bolder",
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"type": "TextBlock",
|
|
1032
|
+
"text": f"{checkpoint_result.data_asset or 'N/A'} | {status.upper()}",
|
|
1033
|
+
"size": "small",
|
|
1034
|
+
"isSubtle": True,
|
|
1035
|
+
"spacing": "none",
|
|
1036
|
+
},
|
|
1037
|
+
],
|
|
1038
|
+
},
|
|
1039
|
+
]
|
|
1040
|
+
|
|
1041
|
+
if stats:
|
|
1042
|
+
columns.append({
|
|
1043
|
+
"type": "Column",
|
|
1044
|
+
"width": "auto",
|
|
1045
|
+
"items": [
|
|
1046
|
+
{
|
|
1047
|
+
"type": "TextBlock",
|
|
1048
|
+
"text": f"{stats.total_issues}",
|
|
1049
|
+
"size": "extraLarge",
|
|
1050
|
+
"weight": "bolder",
|
|
1051
|
+
"color": "attention" if stats.total_issues > 0 else "good",
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"type": "TextBlock",
|
|
1055
|
+
"text": "issues",
|
|
1056
|
+
"size": "small",
|
|
1057
|
+
"isSubtle": True,
|
|
1058
|
+
"spacing": "none",
|
|
1059
|
+
},
|
|
1060
|
+
],
|
|
1061
|
+
"verticalContentAlignment": "center",
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
builder = AdaptiveCardBuilder(config.card_version)
|
|
1065
|
+
builder.add_column_set(columns)
|
|
1066
|
+
|
|
1067
|
+
return builder.build_message_card()
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
# Template registry
|
|
1071
|
+
_TEMPLATE_REGISTRY: dict[str | MessageTheme, type[MessageTemplate]] = {
|
|
1072
|
+
MessageTheme.DEFAULT: DefaultTemplate,
|
|
1073
|
+
MessageTheme.MINIMAL: MinimalTemplate,
|
|
1074
|
+
MessageTheme.DETAILED: DetailedTemplate,
|
|
1075
|
+
MessageTheme.COMPACT: CompactTemplate,
|
|
1076
|
+
"default": DefaultTemplate,
|
|
1077
|
+
"minimal": MinimalTemplate,
|
|
1078
|
+
"detailed": DetailedTemplate,
|
|
1079
|
+
"compact": CompactTemplate,
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def get_template(theme: str | MessageTheme) -> MessageTemplate:
|
|
1084
|
+
"""Get a template instance by theme name.
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
theme: Theme name or MessageTheme enum.
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
Template instance.
|
|
1091
|
+
|
|
1092
|
+
Raises:
|
|
1093
|
+
ValueError: If theme is not found.
|
|
1094
|
+
"""
|
|
1095
|
+
template_class = _TEMPLATE_REGISTRY.get(theme)
|
|
1096
|
+
if template_class is None:
|
|
1097
|
+
raise ValueError(f"Unknown theme: {theme}. Available: {list(_TEMPLATE_REGISTRY.keys())}")
|
|
1098
|
+
return template_class()
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def register_template(name: str, template_class: type[MessageTemplate]) -> None:
|
|
1102
|
+
"""Register a custom template.
|
|
1103
|
+
|
|
1104
|
+
Args:
|
|
1105
|
+
name: Template name.
|
|
1106
|
+
template_class: Template class to register.
|
|
1107
|
+
"""
|
|
1108
|
+
_TEMPLATE_REGISTRY[name] = template_class
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
# =============================================================================
|
|
1112
|
+
# Configuration
|
|
1113
|
+
# =============================================================================
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
@dataclass
|
|
1117
|
+
class TeamsConfig(ActionConfig):
|
|
1118
|
+
"""Configuration for Microsoft Teams notification action.
|
|
1119
|
+
|
|
1120
|
+
Attributes:
|
|
1121
|
+
webhook_url: Teams Incoming Webhook URL.
|
|
1122
|
+
connector_type: Type of connector being used.
|
|
1123
|
+
theme: Message template theme to use.
|
|
1124
|
+
card_version: Adaptive Card version.
|
|
1125
|
+
full_width: Enable full-width card display.
|
|
1126
|
+
include_details: Include detailed statistics.
|
|
1127
|
+
include_actions: Include action buttons.
|
|
1128
|
+
mention_on_failure: Users to @mention on failure.
|
|
1129
|
+
dashboard_url: URL template for dashboard link.
|
|
1130
|
+
details_url: URL template for details link.
|
|
1131
|
+
custom_template: Custom MessageTemplate instance.
|
|
1132
|
+
custom_payload: Completely custom payload (overrides everything).
|
|
1133
|
+
proxy: HTTP proxy URL.
|
|
1134
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
1135
|
+
thread_id: Thread ID for reply (conversation continuity).
|
|
1136
|
+
"""
|
|
1137
|
+
|
|
1138
|
+
webhook_url: str = ""
|
|
1139
|
+
connector_type: TeamsConnectorType | str = TeamsConnectorType.INCOMING_WEBHOOK
|
|
1140
|
+
theme: MessageTheme | str = MessageTheme.DEFAULT
|
|
1141
|
+
card_version: AdaptiveCardVersion | str = AdaptiveCardVersion.V1_4
|
|
1142
|
+
full_width: bool = True
|
|
1143
|
+
include_details: bool = True
|
|
1144
|
+
include_actions: bool = True
|
|
1145
|
+
mention_on_failure: list[str | dict[str, str]] = field(default_factory=list)
|
|
1146
|
+
dashboard_url: str | None = None
|
|
1147
|
+
details_url: str | None = None
|
|
1148
|
+
custom_template: MessageTemplate | None = None
|
|
1149
|
+
custom_payload: dict[str, Any] | None = None
|
|
1150
|
+
proxy: str | None = None
|
|
1151
|
+
verify_ssl: bool = True
|
|
1152
|
+
thread_id: str | None = None
|
|
1153
|
+
notify_on: NotifyCondition | str = NotifyCondition.FAILURE
|
|
1154
|
+
|
|
1155
|
+
def __post_init__(self) -> None:
|
|
1156
|
+
"""Convert string enums to proper types."""
|
|
1157
|
+
super().__post_init__()
|
|
1158
|
+
|
|
1159
|
+
if isinstance(self.connector_type, str):
|
|
1160
|
+
self.connector_type = TeamsConnectorType(self.connector_type)
|
|
1161
|
+
if isinstance(self.theme, str) and self.theme in MessageTheme.__members__.values():
|
|
1162
|
+
self.theme = MessageTheme(self.theme)
|
|
1163
|
+
if isinstance(self.card_version, str):
|
|
1164
|
+
self.card_version = AdaptiveCardVersion(self.card_version)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
# =============================================================================
|
|
1168
|
+
# HTTP Client Abstraction
|
|
1169
|
+
# =============================================================================
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
class TeamsHTTPClient:
|
|
1173
|
+
"""HTTP client for Teams API requests.
|
|
1174
|
+
|
|
1175
|
+
Abstracts HTTP operations for easier testing and future enhancements.
|
|
1176
|
+
"""
|
|
1177
|
+
|
|
1178
|
+
def __init__(
|
|
1179
|
+
self,
|
|
1180
|
+
timeout: int = 30,
|
|
1181
|
+
proxy: str | None = None,
|
|
1182
|
+
verify_ssl: bool = True,
|
|
1183
|
+
) -> None:
|
|
1184
|
+
"""Initialize the HTTP client.
|
|
1185
|
+
|
|
1186
|
+
Args:
|
|
1187
|
+
timeout: Request timeout in seconds.
|
|
1188
|
+
proxy: Proxy URL.
|
|
1189
|
+
verify_ssl: Whether to verify SSL.
|
|
1190
|
+
"""
|
|
1191
|
+
self._timeout = timeout
|
|
1192
|
+
self._proxy = proxy
|
|
1193
|
+
self._verify_ssl = verify_ssl
|
|
1194
|
+
|
|
1195
|
+
def post(
|
|
1196
|
+
self,
|
|
1197
|
+
url: str,
|
|
1198
|
+
payload: dict[str, Any],
|
|
1199
|
+
headers: dict[str, str] | None = None,
|
|
1200
|
+
) -> tuple[int, str]:
|
|
1201
|
+
"""Send a POST request.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
url: Request URL.
|
|
1205
|
+
payload: JSON payload.
|
|
1206
|
+
headers: Optional headers.
|
|
1207
|
+
|
|
1208
|
+
Returns:
|
|
1209
|
+
Tuple of (status_code, response_body).
|
|
1210
|
+
|
|
1211
|
+
Raises:
|
|
1212
|
+
Exception: On request failure.
|
|
1213
|
+
"""
|
|
1214
|
+
import urllib.error
|
|
1215
|
+
import urllib.request
|
|
1216
|
+
import ssl
|
|
1217
|
+
|
|
1218
|
+
default_headers = {
|
|
1219
|
+
"Content-Type": "application/json",
|
|
1220
|
+
"Accept": "application/json",
|
|
1221
|
+
}
|
|
1222
|
+
if headers:
|
|
1223
|
+
default_headers.update(headers)
|
|
1224
|
+
|
|
1225
|
+
data = json.dumps(payload).encode("utf-8")
|
|
1226
|
+
|
|
1227
|
+
request = urllib.request.Request(
|
|
1228
|
+
url,
|
|
1229
|
+
data=data,
|
|
1230
|
+
headers=default_headers,
|
|
1231
|
+
method="POST",
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
# Handle proxy
|
|
1235
|
+
if self._proxy:
|
|
1236
|
+
proxy_handler = urllib.request.ProxyHandler({
|
|
1237
|
+
"http": self._proxy,
|
|
1238
|
+
"https": self._proxy,
|
|
1239
|
+
})
|
|
1240
|
+
opener = urllib.request.build_opener(proxy_handler)
|
|
1241
|
+
else:
|
|
1242
|
+
opener = urllib.request.build_opener()
|
|
1243
|
+
|
|
1244
|
+
# Handle SSL verification
|
|
1245
|
+
context: ssl.SSLContext | None = None
|
|
1246
|
+
if not self._verify_ssl:
|
|
1247
|
+
context = ssl.create_default_context()
|
|
1248
|
+
context.check_hostname = False
|
|
1249
|
+
context.verify_mode = ssl.CERT_NONE
|
|
1250
|
+
|
|
1251
|
+
try:
|
|
1252
|
+
with opener.open(request, timeout=self._timeout, context=context) as response:
|
|
1253
|
+
status_code = response.getcode()
|
|
1254
|
+
body = response.read().decode("utf-8")
|
|
1255
|
+
return status_code, body
|
|
1256
|
+
except urllib.error.HTTPError as e:
|
|
1257
|
+
return e.code, e.read().decode("utf-8") if e.fp else str(e)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
# =============================================================================
|
|
1261
|
+
# Main Action Class
|
|
1262
|
+
# =============================================================================
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
class TeamsNotification(BaseAction[TeamsConfig]):
|
|
1266
|
+
"""Action to send Microsoft Teams notifications.
|
|
1267
|
+
|
|
1268
|
+
Sends formatted Adaptive Card messages to Teams channels via
|
|
1269
|
+
Incoming Webhooks with rich formatting and interactive elements.
|
|
1270
|
+
|
|
1271
|
+
Features:
|
|
1272
|
+
- Rich Adaptive Card formatting
|
|
1273
|
+
- Multiple message themes (default, minimal, detailed, compact)
|
|
1274
|
+
- @mention support
|
|
1275
|
+
- Action buttons for navigation
|
|
1276
|
+
- Thread/reply support
|
|
1277
|
+
- Custom template support
|
|
1278
|
+
- Power Automate/Logic Apps connector support
|
|
1279
|
+
|
|
1280
|
+
Example:
|
|
1281
|
+
>>> action = TeamsNotification(
|
|
1282
|
+
... webhook_url="https://outlook.office.com/webhook/...",
|
|
1283
|
+
... notify_on="failure",
|
|
1284
|
+
... theme="detailed",
|
|
1285
|
+
... mention_on_failure=["user@example.com"],
|
|
1286
|
+
... dashboard_url="https://dashboard.example.com/runs/{run_id}",
|
|
1287
|
+
... )
|
|
1288
|
+
>>> result = action.execute(checkpoint_result)
|
|
1289
|
+
|
|
1290
|
+
Custom Template Example:
|
|
1291
|
+
>>> class MyTemplate(MessageTemplate):
|
|
1292
|
+
... def render(self, checkpoint_result, config):
|
|
1293
|
+
... builder = AdaptiveCardBuilder()
|
|
1294
|
+
... builder.add_text_block("Custom message!")
|
|
1295
|
+
... return builder.build_message_card()
|
|
1296
|
+
>>>
|
|
1297
|
+
>>> action = TeamsNotification(
|
|
1298
|
+
... webhook_url="...",
|
|
1299
|
+
... custom_template=MyTemplate(),
|
|
1300
|
+
... )
|
|
1301
|
+
"""
|
|
1302
|
+
|
|
1303
|
+
action_type = "teams_notification"
|
|
1304
|
+
|
|
1305
|
+
# Webhook URL patterns for validation
|
|
1306
|
+
WEBHOOK_PATTERNS = [
|
|
1307
|
+
r"^https://[a-z0-9-]+\.webhook\.office\.com/",
|
|
1308
|
+
r"^https://outlook\.office\.com/webhook/",
|
|
1309
|
+
r"^https://[a-z0-9-]+\.logic\.azure\.com/",
|
|
1310
|
+
r"^https://prod-\d+\.[a-z]+\.logic\.azure\.com/",
|
|
1311
|
+
]
|
|
1312
|
+
|
|
1313
|
+
@classmethod
|
|
1314
|
+
def _default_config(cls) -> TeamsConfig:
|
|
1315
|
+
"""Create default configuration."""
|
|
1316
|
+
return TeamsConfig()
|
|
1317
|
+
|
|
1318
|
+
def _execute(self, checkpoint_result: "CheckpointResult") -> ActionResult:
|
|
1319
|
+
"""Send Teams notification.
|
|
1320
|
+
|
|
1321
|
+
Args:
|
|
1322
|
+
checkpoint_result: The checkpoint result to notify about.
|
|
1323
|
+
|
|
1324
|
+
Returns:
|
|
1325
|
+
ActionResult with execution status.
|
|
1326
|
+
"""
|
|
1327
|
+
config = self._config
|
|
1328
|
+
|
|
1329
|
+
if not config.webhook_url:
|
|
1330
|
+
return ActionResult(
|
|
1331
|
+
action_name=self.name,
|
|
1332
|
+
action_type=self.action_type,
|
|
1333
|
+
status=ActionStatus.ERROR,
|
|
1334
|
+
message="No webhook URL configured",
|
|
1335
|
+
error="webhook_url is required",
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
# Build message payload
|
|
1339
|
+
try:
|
|
1340
|
+
payload = self._build_payload(checkpoint_result)
|
|
1341
|
+
except Exception as e:
|
|
1342
|
+
return ActionResult(
|
|
1343
|
+
action_name=self.name,
|
|
1344
|
+
action_type=self.action_type,
|
|
1345
|
+
status=ActionStatus.ERROR,
|
|
1346
|
+
message="Failed to build message payload",
|
|
1347
|
+
error=str(e),
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
# Send to Teams
|
|
1351
|
+
client = TeamsHTTPClient(
|
|
1352
|
+
timeout=config.timeout_seconds,
|
|
1353
|
+
proxy=config.proxy,
|
|
1354
|
+
verify_ssl=config.verify_ssl,
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
try:
|
|
1358
|
+
status_code, response_body = client.post(config.webhook_url, payload)
|
|
1359
|
+
|
|
1360
|
+
# Teams returns "1" on success for webhooks
|
|
1361
|
+
if status_code in (200, 201, 202) or response_body == "1":
|
|
1362
|
+
return ActionResult(
|
|
1363
|
+
action_name=self.name,
|
|
1364
|
+
action_type=self.action_type,
|
|
1365
|
+
status=ActionStatus.SUCCESS,
|
|
1366
|
+
message="Teams notification sent",
|
|
1367
|
+
details={
|
|
1368
|
+
"status_code": status_code,
|
|
1369
|
+
"response": response_body[:200] if response_body else None,
|
|
1370
|
+
"theme": str(config.theme),
|
|
1371
|
+
"connector_type": str(config.connector_type),
|
|
1372
|
+
},
|
|
1373
|
+
)
|
|
1374
|
+
else:
|
|
1375
|
+
return ActionResult(
|
|
1376
|
+
action_name=self.name,
|
|
1377
|
+
action_type=self.action_type,
|
|
1378
|
+
status=ActionStatus.ERROR,
|
|
1379
|
+
message=f"Teams webhook returned error: {status_code}",
|
|
1380
|
+
error=response_body[:500] if response_body else "Unknown error",
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
except Exception as e:
|
|
1384
|
+
return ActionResult(
|
|
1385
|
+
action_name=self.name,
|
|
1386
|
+
action_type=self.action_type,
|
|
1387
|
+
status=ActionStatus.ERROR,
|
|
1388
|
+
message="Failed to send Teams notification",
|
|
1389
|
+
error=str(e),
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
def _build_payload(self, checkpoint_result: "CheckpointResult") -> dict[str, Any]:
|
|
1393
|
+
"""Build the Teams message payload.
|
|
1394
|
+
|
|
1395
|
+
Args:
|
|
1396
|
+
checkpoint_result: The checkpoint result.
|
|
1397
|
+
|
|
1398
|
+
Returns:
|
|
1399
|
+
Message payload dictionary.
|
|
1400
|
+
"""
|
|
1401
|
+
config = self._config
|
|
1402
|
+
|
|
1403
|
+
# Use custom payload if provided
|
|
1404
|
+
if config.custom_payload:
|
|
1405
|
+
return self._substitute_placeholders(config.custom_payload, checkpoint_result)
|
|
1406
|
+
|
|
1407
|
+
# Use custom template if provided
|
|
1408
|
+
if config.custom_template:
|
|
1409
|
+
return config.custom_template.render(checkpoint_result, config)
|
|
1410
|
+
|
|
1411
|
+
# Use theme-based template
|
|
1412
|
+
template = get_template(config.theme)
|
|
1413
|
+
return template.render(checkpoint_result, config)
|
|
1414
|
+
|
|
1415
|
+
def _substitute_placeholders(
|
|
1416
|
+
self,
|
|
1417
|
+
payload: dict[str, Any],
|
|
1418
|
+
checkpoint_result: "CheckpointResult",
|
|
1419
|
+
) -> dict[str, Any]:
|
|
1420
|
+
"""Substitute placeholders in custom payload.
|
|
1421
|
+
|
|
1422
|
+
Args:
|
|
1423
|
+
payload: Payload with placeholders.
|
|
1424
|
+
checkpoint_result: Data for substitution.
|
|
1425
|
+
|
|
1426
|
+
Returns:
|
|
1427
|
+
Payload with substituted values.
|
|
1428
|
+
"""
|
|
1429
|
+
validation = checkpoint_result.validation_result
|
|
1430
|
+
stats = validation.statistics if validation else None
|
|
1431
|
+
|
|
1432
|
+
substitutions = {
|
|
1433
|
+
"${checkpoint}": checkpoint_result.checkpoint_name,
|
|
1434
|
+
"${status}": checkpoint_result.status.value,
|
|
1435
|
+
"${run_id}": checkpoint_result.run_id,
|
|
1436
|
+
"${data_asset}": checkpoint_result.data_asset or "",
|
|
1437
|
+
"${run_time}": checkpoint_result.run_time.isoformat(),
|
|
1438
|
+
"${duration_ms}": str(checkpoint_result.duration_ms),
|
|
1439
|
+
"${total_issues}": str(stats.total_issues) if stats else "0",
|
|
1440
|
+
"${critical_issues}": str(stats.critical_issues) if stats else "0",
|
|
1441
|
+
"${high_issues}": str(stats.high_issues) if stats else "0",
|
|
1442
|
+
"${medium_issues}": str(stats.medium_issues) if stats else "0",
|
|
1443
|
+
"${low_issues}": str(stats.low_issues) if stats else "0",
|
|
1444
|
+
"${pass_rate}": f"{stats.pass_rate * 100:.1f}" if stats else "100.0",
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
def substitute(obj: Any) -> Any:
|
|
1448
|
+
if isinstance(obj, str):
|
|
1449
|
+
for placeholder, value in substitutions.items():
|
|
1450
|
+
obj = obj.replace(placeholder, value)
|
|
1451
|
+
return obj
|
|
1452
|
+
elif isinstance(obj, dict):
|
|
1453
|
+
return {k: substitute(v) for k, v in obj.items()}
|
|
1454
|
+
elif isinstance(obj, list):
|
|
1455
|
+
return [substitute(item) for item in obj]
|
|
1456
|
+
return obj
|
|
1457
|
+
|
|
1458
|
+
return substitute(payload)
|
|
1459
|
+
|
|
1460
|
+
def validate_config(self) -> list[str]:
|
|
1461
|
+
"""Validate configuration.
|
|
1462
|
+
|
|
1463
|
+
Returns:
|
|
1464
|
+
List of validation error messages.
|
|
1465
|
+
"""
|
|
1466
|
+
errors: list[str] = []
|
|
1467
|
+
|
|
1468
|
+
if not self._config.webhook_url:
|
|
1469
|
+
errors.append("webhook_url is required")
|
|
1470
|
+
else:
|
|
1471
|
+
# Validate webhook URL pattern
|
|
1472
|
+
is_valid = any(
|
|
1473
|
+
re.match(pattern, self._config.webhook_url)
|
|
1474
|
+
for pattern in self.WEBHOOK_PATTERNS
|
|
1475
|
+
)
|
|
1476
|
+
if not is_valid:
|
|
1477
|
+
errors.append(
|
|
1478
|
+
"webhook_url does not appear to be a valid Teams webhook URL. "
|
|
1479
|
+
"Expected patterns: outlook.office.com/webhook/*, *.webhook.office.com/*, "
|
|
1480
|
+
"*.logic.azure.com/*"
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
# Validate theme
|
|
1484
|
+
if isinstance(self._config.theme, str) and self._config.theme not in _TEMPLATE_REGISTRY:
|
|
1485
|
+
errors.append(f"Unknown theme: {self._config.theme}")
|
|
1486
|
+
|
|
1487
|
+
return errors
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
# =============================================================================
|
|
1491
|
+
# Convenience Functions
|
|
1492
|
+
# =============================================================================
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def create_teams_notification(
|
|
1496
|
+
webhook_url: str,
|
|
1497
|
+
*,
|
|
1498
|
+
notify_on: str | NotifyCondition = NotifyCondition.FAILURE,
|
|
1499
|
+
theme: str | MessageTheme = MessageTheme.DEFAULT,
|
|
1500
|
+
**kwargs: Any,
|
|
1501
|
+
) -> TeamsNotification:
|
|
1502
|
+
"""Create a Teams notification action with common defaults.
|
|
1503
|
+
|
|
1504
|
+
Args:
|
|
1505
|
+
webhook_url: Teams webhook URL.
|
|
1506
|
+
notify_on: When to notify.
|
|
1507
|
+
theme: Message theme.
|
|
1508
|
+
**kwargs: Additional configuration options.
|
|
1509
|
+
|
|
1510
|
+
Returns:
|
|
1511
|
+
Configured TeamsNotification instance.
|
|
1512
|
+
"""
|
|
1513
|
+
return TeamsNotification(
|
|
1514
|
+
webhook_url=webhook_url,
|
|
1515
|
+
notify_on=notify_on,
|
|
1516
|
+
theme=theme,
|
|
1517
|
+
**kwargs,
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def create_failure_alert(
|
|
1522
|
+
webhook_url: str,
|
|
1523
|
+
mention_users: list[str] | None = None,
|
|
1524
|
+
dashboard_url: str | None = None,
|
|
1525
|
+
) -> TeamsNotification:
|
|
1526
|
+
"""Create a Teams notification for failure alerts.
|
|
1527
|
+
|
|
1528
|
+
Pre-configured for failure notifications with mentions and dashboard link.
|
|
1529
|
+
|
|
1530
|
+
Args:
|
|
1531
|
+
webhook_url: Teams webhook URL.
|
|
1532
|
+
mention_users: Users to @mention on failure.
|
|
1533
|
+
dashboard_url: Dashboard URL template.
|
|
1534
|
+
|
|
1535
|
+
Returns:
|
|
1536
|
+
Configured TeamsNotification for failures.
|
|
1537
|
+
"""
|
|
1538
|
+
return TeamsNotification(
|
|
1539
|
+
webhook_url=webhook_url,
|
|
1540
|
+
notify_on=NotifyCondition.FAILURE_OR_ERROR,
|
|
1541
|
+
theme=MessageTheme.DETAILED,
|
|
1542
|
+
mention_on_failure=mention_users or [],
|
|
1543
|
+
dashboard_url=dashboard_url,
|
|
1544
|
+
include_actions=True,
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def create_summary_notification(
|
|
1549
|
+
webhook_url: str,
|
|
1550
|
+
*,
|
|
1551
|
+
notify_on: str | NotifyCondition = NotifyCondition.ALWAYS,
|
|
1552
|
+
) -> TeamsNotification:
|
|
1553
|
+
"""Create a Teams notification for summary reports.
|
|
1554
|
+
|
|
1555
|
+
Pre-configured for compact summary notifications.
|
|
1556
|
+
|
|
1557
|
+
Args:
|
|
1558
|
+
webhook_url: Teams webhook URL.
|
|
1559
|
+
notify_on: When to notify.
|
|
1560
|
+
|
|
1561
|
+
Returns:
|
|
1562
|
+
Configured TeamsNotification for summaries.
|
|
1563
|
+
"""
|
|
1564
|
+
return TeamsNotification(
|
|
1565
|
+
webhook_url=webhook_url,
|
|
1566
|
+
notify_on=notify_on,
|
|
1567
|
+
theme=MessageTheme.COMPACT,
|
|
1568
|
+
include_details=False,
|
|
1569
|
+
include_actions=False,
|
|
1570
|
+
)
|