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.
Files changed (877) hide show
  1. truthound/__init__.py +162 -0
  2. truthound/adapters.py +100 -0
  3. truthound/api.py +365 -0
  4. truthound/audit/__init__.py +248 -0
  5. truthound/audit/core.py +967 -0
  6. truthound/audit/filters.py +620 -0
  7. truthound/audit/formatters.py +707 -0
  8. truthound/audit/logger.py +902 -0
  9. truthound/audit/middleware.py +571 -0
  10. truthound/audit/storage.py +1083 -0
  11. truthound/benchmark/__init__.py +123 -0
  12. truthound/benchmark/base.py +757 -0
  13. truthound/benchmark/comparison.py +635 -0
  14. truthound/benchmark/generators.py +706 -0
  15. truthound/benchmark/reporters.py +718 -0
  16. truthound/benchmark/runner.py +635 -0
  17. truthound/benchmark/scenarios.py +712 -0
  18. truthound/cache.py +252 -0
  19. truthound/checkpoint/__init__.py +136 -0
  20. truthound/checkpoint/actions/__init__.py +164 -0
  21. truthound/checkpoint/actions/base.py +324 -0
  22. truthound/checkpoint/actions/custom.py +234 -0
  23. truthound/checkpoint/actions/discord_notify.py +290 -0
  24. truthound/checkpoint/actions/email_notify.py +405 -0
  25. truthound/checkpoint/actions/github_action.py +406 -0
  26. truthound/checkpoint/actions/opsgenie.py +1499 -0
  27. truthound/checkpoint/actions/pagerduty.py +226 -0
  28. truthound/checkpoint/actions/slack_notify.py +233 -0
  29. truthound/checkpoint/actions/store_result.py +249 -0
  30. truthound/checkpoint/actions/teams_notify.py +1570 -0
  31. truthound/checkpoint/actions/telegram_notify.py +419 -0
  32. truthound/checkpoint/actions/update_docs.py +552 -0
  33. truthound/checkpoint/actions/webhook.py +293 -0
  34. truthound/checkpoint/analytics/__init__.py +147 -0
  35. truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
  36. truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
  37. truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
  38. truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
  39. truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
  40. truthound/checkpoint/analytics/analyzers/base.py +270 -0
  41. truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
  42. truthound/checkpoint/analytics/analyzers/trend.py +314 -0
  43. truthound/checkpoint/analytics/models.py +292 -0
  44. truthound/checkpoint/analytics/protocols.py +549 -0
  45. truthound/checkpoint/analytics/service.py +718 -0
  46. truthound/checkpoint/analytics/stores/__init__.py +16 -0
  47. truthound/checkpoint/analytics/stores/base.py +306 -0
  48. truthound/checkpoint/analytics/stores/memory_store.py +353 -0
  49. truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
  50. truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
  51. truthound/checkpoint/async_actions.py +794 -0
  52. truthound/checkpoint/async_base.py +708 -0
  53. truthound/checkpoint/async_checkpoint.py +617 -0
  54. truthound/checkpoint/async_runner.py +639 -0
  55. truthound/checkpoint/checkpoint.py +527 -0
  56. truthound/checkpoint/ci/__init__.py +61 -0
  57. truthound/checkpoint/ci/detector.py +355 -0
  58. truthound/checkpoint/ci/reporter.py +436 -0
  59. truthound/checkpoint/ci/templates.py +454 -0
  60. truthound/checkpoint/circuitbreaker/__init__.py +133 -0
  61. truthound/checkpoint/circuitbreaker/breaker.py +542 -0
  62. truthound/checkpoint/circuitbreaker/core.py +252 -0
  63. truthound/checkpoint/circuitbreaker/detection.py +459 -0
  64. truthound/checkpoint/circuitbreaker/middleware.py +389 -0
  65. truthound/checkpoint/circuitbreaker/registry.py +357 -0
  66. truthound/checkpoint/distributed/__init__.py +139 -0
  67. truthound/checkpoint/distributed/backends/__init__.py +35 -0
  68. truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
  69. truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
  70. truthound/checkpoint/distributed/backends/local_backend.py +397 -0
  71. truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
  72. truthound/checkpoint/distributed/base.py +774 -0
  73. truthound/checkpoint/distributed/orchestrator.py +765 -0
  74. truthound/checkpoint/distributed/protocols.py +842 -0
  75. truthound/checkpoint/distributed/registry.py +449 -0
  76. truthound/checkpoint/idempotency/__init__.py +120 -0
  77. truthound/checkpoint/idempotency/core.py +295 -0
  78. truthound/checkpoint/idempotency/fingerprint.py +454 -0
  79. truthound/checkpoint/idempotency/locking.py +604 -0
  80. truthound/checkpoint/idempotency/service.py +592 -0
  81. truthound/checkpoint/idempotency/stores.py +653 -0
  82. truthound/checkpoint/monitoring/__init__.py +134 -0
  83. truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
  84. truthound/checkpoint/monitoring/aggregators/base.py +372 -0
  85. truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
  86. truthound/checkpoint/monitoring/aggregators/window.py +493 -0
  87. truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
  88. truthound/checkpoint/monitoring/collectors/base.py +257 -0
  89. truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
  90. truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
  91. truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
  92. truthound/checkpoint/monitoring/events.py +410 -0
  93. truthound/checkpoint/monitoring/protocols.py +636 -0
  94. truthound/checkpoint/monitoring/service.py +578 -0
  95. truthound/checkpoint/monitoring/views/__init__.py +17 -0
  96. truthound/checkpoint/monitoring/views/base.py +172 -0
  97. truthound/checkpoint/monitoring/views/queue_view.py +220 -0
  98. truthound/checkpoint/monitoring/views/task_view.py +240 -0
  99. truthound/checkpoint/monitoring/views/worker_view.py +263 -0
  100. truthound/checkpoint/registry.py +337 -0
  101. truthound/checkpoint/runner.py +356 -0
  102. truthound/checkpoint/transaction/__init__.py +133 -0
  103. truthound/checkpoint/transaction/base.py +389 -0
  104. truthound/checkpoint/transaction/compensatable.py +537 -0
  105. truthound/checkpoint/transaction/coordinator.py +576 -0
  106. truthound/checkpoint/transaction/executor.py +622 -0
  107. truthound/checkpoint/transaction/idempotency.py +534 -0
  108. truthound/checkpoint/transaction/saga/__init__.py +143 -0
  109. truthound/checkpoint/transaction/saga/builder.py +584 -0
  110. truthound/checkpoint/transaction/saga/definition.py +515 -0
  111. truthound/checkpoint/transaction/saga/event_store.py +542 -0
  112. truthound/checkpoint/transaction/saga/patterns.py +833 -0
  113. truthound/checkpoint/transaction/saga/runner.py +718 -0
  114. truthound/checkpoint/transaction/saga/state_machine.py +793 -0
  115. truthound/checkpoint/transaction/saga/strategies.py +780 -0
  116. truthound/checkpoint/transaction/saga/testing.py +886 -0
  117. truthound/checkpoint/triggers/__init__.py +58 -0
  118. truthound/checkpoint/triggers/base.py +237 -0
  119. truthound/checkpoint/triggers/event.py +385 -0
  120. truthound/checkpoint/triggers/schedule.py +355 -0
  121. truthound/cli.py +2358 -0
  122. truthound/cli_modules/__init__.py +124 -0
  123. truthound/cli_modules/advanced/__init__.py +45 -0
  124. truthound/cli_modules/advanced/benchmark.py +343 -0
  125. truthound/cli_modules/advanced/docs.py +225 -0
  126. truthound/cli_modules/advanced/lineage.py +209 -0
  127. truthound/cli_modules/advanced/ml.py +320 -0
  128. truthound/cli_modules/advanced/realtime.py +196 -0
  129. truthound/cli_modules/checkpoint/__init__.py +46 -0
  130. truthound/cli_modules/checkpoint/init.py +114 -0
  131. truthound/cli_modules/checkpoint/list.py +71 -0
  132. truthound/cli_modules/checkpoint/run.py +159 -0
  133. truthound/cli_modules/checkpoint/validate.py +67 -0
  134. truthound/cli_modules/common/__init__.py +71 -0
  135. truthound/cli_modules/common/errors.py +414 -0
  136. truthound/cli_modules/common/options.py +419 -0
  137. truthound/cli_modules/common/output.py +507 -0
  138. truthound/cli_modules/common/protocol.py +552 -0
  139. truthound/cli_modules/core/__init__.py +48 -0
  140. truthound/cli_modules/core/check.py +123 -0
  141. truthound/cli_modules/core/compare.py +104 -0
  142. truthound/cli_modules/core/learn.py +57 -0
  143. truthound/cli_modules/core/mask.py +77 -0
  144. truthound/cli_modules/core/profile.py +65 -0
  145. truthound/cli_modules/core/scan.py +61 -0
  146. truthound/cli_modules/profiler/__init__.py +51 -0
  147. truthound/cli_modules/profiler/auto_profile.py +175 -0
  148. truthound/cli_modules/profiler/metadata.py +107 -0
  149. truthound/cli_modules/profiler/suite.py +283 -0
  150. truthound/cli_modules/registry.py +431 -0
  151. truthound/cli_modules/scaffolding/__init__.py +89 -0
  152. truthound/cli_modules/scaffolding/base.py +631 -0
  153. truthound/cli_modules/scaffolding/commands.py +545 -0
  154. truthound/cli_modules/scaffolding/plugins.py +1072 -0
  155. truthound/cli_modules/scaffolding/reporters.py +594 -0
  156. truthound/cli_modules/scaffolding/validators.py +1127 -0
  157. truthound/common/__init__.py +18 -0
  158. truthound/common/resilience/__init__.py +130 -0
  159. truthound/common/resilience/bulkhead.py +266 -0
  160. truthound/common/resilience/circuit_breaker.py +516 -0
  161. truthound/common/resilience/composite.py +332 -0
  162. truthound/common/resilience/config.py +292 -0
  163. truthound/common/resilience/protocols.py +217 -0
  164. truthound/common/resilience/rate_limiter.py +404 -0
  165. truthound/common/resilience/retry.py +341 -0
  166. truthound/datadocs/__init__.py +260 -0
  167. truthound/datadocs/base.py +571 -0
  168. truthound/datadocs/builder.py +761 -0
  169. truthound/datadocs/charts.py +764 -0
  170. truthound/datadocs/dashboard/__init__.py +63 -0
  171. truthound/datadocs/dashboard/app.py +576 -0
  172. truthound/datadocs/dashboard/components.py +584 -0
  173. truthound/datadocs/dashboard/state.py +240 -0
  174. truthound/datadocs/engine/__init__.py +46 -0
  175. truthound/datadocs/engine/context.py +376 -0
  176. truthound/datadocs/engine/pipeline.py +618 -0
  177. truthound/datadocs/engine/registry.py +469 -0
  178. truthound/datadocs/exporters/__init__.py +49 -0
  179. truthound/datadocs/exporters/base.py +198 -0
  180. truthound/datadocs/exporters/html.py +178 -0
  181. truthound/datadocs/exporters/json_exporter.py +253 -0
  182. truthound/datadocs/exporters/markdown.py +284 -0
  183. truthound/datadocs/exporters/pdf.py +392 -0
  184. truthound/datadocs/i18n/__init__.py +86 -0
  185. truthound/datadocs/i18n/catalog.py +960 -0
  186. truthound/datadocs/i18n/formatting.py +505 -0
  187. truthound/datadocs/i18n/loader.py +256 -0
  188. truthound/datadocs/i18n/plurals.py +378 -0
  189. truthound/datadocs/renderers/__init__.py +42 -0
  190. truthound/datadocs/renderers/base.py +401 -0
  191. truthound/datadocs/renderers/custom.py +342 -0
  192. truthound/datadocs/renderers/jinja.py +697 -0
  193. truthound/datadocs/sections.py +736 -0
  194. truthound/datadocs/styles.py +931 -0
  195. truthound/datadocs/themes/__init__.py +101 -0
  196. truthound/datadocs/themes/base.py +336 -0
  197. truthound/datadocs/themes/default.py +417 -0
  198. truthound/datadocs/themes/enterprise.py +419 -0
  199. truthound/datadocs/themes/loader.py +336 -0
  200. truthound/datadocs/themes.py +301 -0
  201. truthound/datadocs/transformers/__init__.py +57 -0
  202. truthound/datadocs/transformers/base.py +268 -0
  203. truthound/datadocs/transformers/enrichers.py +544 -0
  204. truthound/datadocs/transformers/filters.py +447 -0
  205. truthound/datadocs/transformers/i18n.py +468 -0
  206. truthound/datadocs/versioning/__init__.py +62 -0
  207. truthound/datadocs/versioning/diff.py +639 -0
  208. truthound/datadocs/versioning/storage.py +497 -0
  209. truthound/datadocs/versioning/version.py +358 -0
  210. truthound/datasources/__init__.py +223 -0
  211. truthound/datasources/_async_protocols.py +222 -0
  212. truthound/datasources/_protocols.py +159 -0
  213. truthound/datasources/adapters.py +428 -0
  214. truthound/datasources/async_base.py +599 -0
  215. truthound/datasources/async_factory.py +511 -0
  216. truthound/datasources/base.py +516 -0
  217. truthound/datasources/factory.py +433 -0
  218. truthound/datasources/nosql/__init__.py +47 -0
  219. truthound/datasources/nosql/base.py +487 -0
  220. truthound/datasources/nosql/elasticsearch.py +801 -0
  221. truthound/datasources/nosql/mongodb.py +636 -0
  222. truthound/datasources/pandas_optimized.py +582 -0
  223. truthound/datasources/pandas_source.py +216 -0
  224. truthound/datasources/polars_source.py +395 -0
  225. truthound/datasources/spark_source.py +479 -0
  226. truthound/datasources/sql/__init__.py +154 -0
  227. truthound/datasources/sql/base.py +710 -0
  228. truthound/datasources/sql/bigquery.py +410 -0
  229. truthound/datasources/sql/cloud_base.py +199 -0
  230. truthound/datasources/sql/databricks.py +471 -0
  231. truthound/datasources/sql/mysql.py +316 -0
  232. truthound/datasources/sql/oracle.py +427 -0
  233. truthound/datasources/sql/postgresql.py +321 -0
  234. truthound/datasources/sql/redshift.py +479 -0
  235. truthound/datasources/sql/snowflake.py +439 -0
  236. truthound/datasources/sql/sqlite.py +286 -0
  237. truthound/datasources/sql/sqlserver.py +437 -0
  238. truthound/datasources/streaming/__init__.py +47 -0
  239. truthound/datasources/streaming/base.py +350 -0
  240. truthound/datasources/streaming/kafka.py +670 -0
  241. truthound/decorators.py +98 -0
  242. truthound/docs/__init__.py +69 -0
  243. truthound/docs/extractor.py +971 -0
  244. truthound/docs/generator.py +601 -0
  245. truthound/docs/parser.py +1037 -0
  246. truthound/docs/renderer.py +999 -0
  247. truthound/drift/__init__.py +22 -0
  248. truthound/drift/compare.py +189 -0
  249. truthound/drift/detectors.py +464 -0
  250. truthound/drift/report.py +160 -0
  251. truthound/execution/__init__.py +65 -0
  252. truthound/execution/_protocols.py +324 -0
  253. truthound/execution/base.py +576 -0
  254. truthound/execution/distributed/__init__.py +179 -0
  255. truthound/execution/distributed/aggregations.py +731 -0
  256. truthound/execution/distributed/arrow_bridge.py +817 -0
  257. truthound/execution/distributed/base.py +550 -0
  258. truthound/execution/distributed/dask_engine.py +976 -0
  259. truthound/execution/distributed/mixins.py +766 -0
  260. truthound/execution/distributed/protocols.py +756 -0
  261. truthound/execution/distributed/ray_engine.py +1127 -0
  262. truthound/execution/distributed/registry.py +446 -0
  263. truthound/execution/distributed/spark_engine.py +1011 -0
  264. truthound/execution/distributed/validator_adapter.py +682 -0
  265. truthound/execution/pandas_engine.py +401 -0
  266. truthound/execution/polars_engine.py +497 -0
  267. truthound/execution/pushdown/__init__.py +230 -0
  268. truthound/execution/pushdown/ast.py +1550 -0
  269. truthound/execution/pushdown/builder.py +1550 -0
  270. truthound/execution/pushdown/dialects.py +1072 -0
  271. truthound/execution/pushdown/executor.py +829 -0
  272. truthound/execution/pushdown/optimizer.py +1041 -0
  273. truthound/execution/sql_engine.py +518 -0
  274. truthound/infrastructure/__init__.py +189 -0
  275. truthound/infrastructure/audit.py +1515 -0
  276. truthound/infrastructure/config.py +1133 -0
  277. truthound/infrastructure/encryption.py +1132 -0
  278. truthound/infrastructure/logging.py +1503 -0
  279. truthound/infrastructure/metrics.py +1220 -0
  280. truthound/lineage/__init__.py +89 -0
  281. truthound/lineage/base.py +746 -0
  282. truthound/lineage/impact_analysis.py +474 -0
  283. truthound/lineage/integrations/__init__.py +22 -0
  284. truthound/lineage/integrations/openlineage.py +548 -0
  285. truthound/lineage/tracker.py +512 -0
  286. truthound/lineage/visualization/__init__.py +33 -0
  287. truthound/lineage/visualization/protocols.py +145 -0
  288. truthound/lineage/visualization/renderers/__init__.py +20 -0
  289. truthound/lineage/visualization/renderers/cytoscape.py +329 -0
  290. truthound/lineage/visualization/renderers/d3.py +331 -0
  291. truthound/lineage/visualization/renderers/graphviz.py +276 -0
  292. truthound/lineage/visualization/renderers/mermaid.py +308 -0
  293. truthound/maskers.py +113 -0
  294. truthound/ml/__init__.py +124 -0
  295. truthound/ml/anomaly_models/__init__.py +31 -0
  296. truthound/ml/anomaly_models/ensemble.py +362 -0
  297. truthound/ml/anomaly_models/isolation_forest.py +444 -0
  298. truthound/ml/anomaly_models/statistical.py +392 -0
  299. truthound/ml/base.py +1178 -0
  300. truthound/ml/drift_detection/__init__.py +26 -0
  301. truthound/ml/drift_detection/concept.py +381 -0
  302. truthound/ml/drift_detection/distribution.py +361 -0
  303. truthound/ml/drift_detection/feature.py +442 -0
  304. truthound/ml/drift_detection/multivariate.py +495 -0
  305. truthound/ml/monitoring/__init__.py +88 -0
  306. truthound/ml/monitoring/alerting/__init__.py +33 -0
  307. truthound/ml/monitoring/alerting/handlers.py +427 -0
  308. truthound/ml/monitoring/alerting/rules.py +508 -0
  309. truthound/ml/monitoring/collectors/__init__.py +19 -0
  310. truthound/ml/monitoring/collectors/composite.py +105 -0
  311. truthound/ml/monitoring/collectors/drift.py +324 -0
  312. truthound/ml/monitoring/collectors/performance.py +179 -0
  313. truthound/ml/monitoring/collectors/quality.py +369 -0
  314. truthound/ml/monitoring/monitor.py +536 -0
  315. truthound/ml/monitoring/protocols.py +451 -0
  316. truthound/ml/monitoring/stores/__init__.py +15 -0
  317. truthound/ml/monitoring/stores/memory.py +201 -0
  318. truthound/ml/monitoring/stores/prometheus.py +296 -0
  319. truthound/ml/rule_learning/__init__.py +25 -0
  320. truthound/ml/rule_learning/constraint_miner.py +443 -0
  321. truthound/ml/rule_learning/pattern_learner.py +499 -0
  322. truthound/ml/rule_learning/profile_learner.py +462 -0
  323. truthound/multitenancy/__init__.py +326 -0
  324. truthound/multitenancy/core.py +852 -0
  325. truthound/multitenancy/integration.py +597 -0
  326. truthound/multitenancy/isolation.py +630 -0
  327. truthound/multitenancy/manager.py +770 -0
  328. truthound/multitenancy/middleware.py +765 -0
  329. truthound/multitenancy/quota.py +537 -0
  330. truthound/multitenancy/resolvers.py +603 -0
  331. truthound/multitenancy/storage.py +703 -0
  332. truthound/observability/__init__.py +307 -0
  333. truthound/observability/context.py +531 -0
  334. truthound/observability/instrumentation.py +611 -0
  335. truthound/observability/logging.py +887 -0
  336. truthound/observability/metrics.py +1157 -0
  337. truthound/observability/tracing/__init__.py +178 -0
  338. truthound/observability/tracing/baggage.py +310 -0
  339. truthound/observability/tracing/config.py +426 -0
  340. truthound/observability/tracing/exporter.py +787 -0
  341. truthound/observability/tracing/integration.py +1018 -0
  342. truthound/observability/tracing/otel/__init__.py +146 -0
  343. truthound/observability/tracing/otel/adapter.py +982 -0
  344. truthound/observability/tracing/otel/bridge.py +1177 -0
  345. truthound/observability/tracing/otel/compat.py +681 -0
  346. truthound/observability/tracing/otel/config.py +691 -0
  347. truthound/observability/tracing/otel/detection.py +327 -0
  348. truthound/observability/tracing/otel/protocols.py +426 -0
  349. truthound/observability/tracing/processor.py +561 -0
  350. truthound/observability/tracing/propagator.py +757 -0
  351. truthound/observability/tracing/provider.py +569 -0
  352. truthound/observability/tracing/resource.py +515 -0
  353. truthound/observability/tracing/sampler.py +487 -0
  354. truthound/observability/tracing/span.py +676 -0
  355. truthound/plugins/__init__.py +198 -0
  356. truthound/plugins/base.py +599 -0
  357. truthound/plugins/cli.py +680 -0
  358. truthound/plugins/dependencies/__init__.py +42 -0
  359. truthound/plugins/dependencies/graph.py +422 -0
  360. truthound/plugins/dependencies/resolver.py +417 -0
  361. truthound/plugins/discovery.py +379 -0
  362. truthound/plugins/docs/__init__.py +46 -0
  363. truthound/plugins/docs/extractor.py +444 -0
  364. truthound/plugins/docs/renderer.py +499 -0
  365. truthound/plugins/enterprise_manager.py +877 -0
  366. truthound/plugins/examples/__init__.py +19 -0
  367. truthound/plugins/examples/custom_validators.py +317 -0
  368. truthound/plugins/examples/slack_notifier.py +312 -0
  369. truthound/plugins/examples/xml_reporter.py +254 -0
  370. truthound/plugins/hooks.py +558 -0
  371. truthound/plugins/lifecycle/__init__.py +43 -0
  372. truthound/plugins/lifecycle/hot_reload.py +402 -0
  373. truthound/plugins/lifecycle/manager.py +371 -0
  374. truthound/plugins/manager.py +736 -0
  375. truthound/plugins/registry.py +338 -0
  376. truthound/plugins/security/__init__.py +93 -0
  377. truthound/plugins/security/exceptions.py +332 -0
  378. truthound/plugins/security/policies.py +348 -0
  379. truthound/plugins/security/protocols.py +643 -0
  380. truthound/plugins/security/sandbox/__init__.py +45 -0
  381. truthound/plugins/security/sandbox/context.py +158 -0
  382. truthound/plugins/security/sandbox/engines/__init__.py +19 -0
  383. truthound/plugins/security/sandbox/engines/container.py +379 -0
  384. truthound/plugins/security/sandbox/engines/noop.py +144 -0
  385. truthound/plugins/security/sandbox/engines/process.py +336 -0
  386. truthound/plugins/security/sandbox/factory.py +211 -0
  387. truthound/plugins/security/signing/__init__.py +57 -0
  388. truthound/plugins/security/signing/service.py +330 -0
  389. truthound/plugins/security/signing/trust_store.py +368 -0
  390. truthound/plugins/security/signing/verifier.py +459 -0
  391. truthound/plugins/versioning/__init__.py +41 -0
  392. truthound/plugins/versioning/constraints.py +297 -0
  393. truthound/plugins/versioning/resolver.py +329 -0
  394. truthound/profiler/__init__.py +1729 -0
  395. truthound/profiler/_lazy.py +452 -0
  396. truthound/profiler/ab_testing/__init__.py +80 -0
  397. truthound/profiler/ab_testing/analysis.py +449 -0
  398. truthound/profiler/ab_testing/base.py +257 -0
  399. truthound/profiler/ab_testing/experiment.py +395 -0
  400. truthound/profiler/ab_testing/tracking.py +368 -0
  401. truthound/profiler/auto_threshold.py +1170 -0
  402. truthound/profiler/base.py +579 -0
  403. truthound/profiler/cache_patterns.py +911 -0
  404. truthound/profiler/caching.py +1303 -0
  405. truthound/profiler/column_profiler.py +712 -0
  406. truthound/profiler/comparison.py +1007 -0
  407. truthound/profiler/custom_patterns.py +1170 -0
  408. truthound/profiler/dashboard/__init__.py +50 -0
  409. truthound/profiler/dashboard/app.py +476 -0
  410. truthound/profiler/dashboard/components.py +457 -0
  411. truthound/profiler/dashboard/config.py +72 -0
  412. truthound/profiler/distributed/__init__.py +83 -0
  413. truthound/profiler/distributed/base.py +281 -0
  414. truthound/profiler/distributed/dask_backend.py +498 -0
  415. truthound/profiler/distributed/local_backend.py +293 -0
  416. truthound/profiler/distributed/profiler.py +304 -0
  417. truthound/profiler/distributed/ray_backend.py +374 -0
  418. truthound/profiler/distributed/spark_backend.py +375 -0
  419. truthound/profiler/distributed.py +1366 -0
  420. truthound/profiler/enterprise_sampling.py +1065 -0
  421. truthound/profiler/errors.py +488 -0
  422. truthound/profiler/evolution/__init__.py +91 -0
  423. truthound/profiler/evolution/alerts.py +426 -0
  424. truthound/profiler/evolution/changes.py +206 -0
  425. truthound/profiler/evolution/compatibility.py +365 -0
  426. truthound/profiler/evolution/detector.py +372 -0
  427. truthound/profiler/evolution/protocols.py +121 -0
  428. truthound/profiler/generators/__init__.py +48 -0
  429. truthound/profiler/generators/base.py +384 -0
  430. truthound/profiler/generators/ml_rules.py +375 -0
  431. truthound/profiler/generators/pattern_rules.py +384 -0
  432. truthound/profiler/generators/schema_rules.py +267 -0
  433. truthound/profiler/generators/stats_rules.py +324 -0
  434. truthound/profiler/generators/suite_generator.py +857 -0
  435. truthound/profiler/i18n.py +1542 -0
  436. truthound/profiler/incremental.py +554 -0
  437. truthound/profiler/incremental_validation.py +1710 -0
  438. truthound/profiler/integration/__init__.py +73 -0
  439. truthound/profiler/integration/adapters.py +345 -0
  440. truthound/profiler/integration/context.py +371 -0
  441. truthound/profiler/integration/executor.py +527 -0
  442. truthound/profiler/integration/naming.py +75 -0
  443. truthound/profiler/integration/protocols.py +243 -0
  444. truthound/profiler/memory.py +1185 -0
  445. truthound/profiler/migration/__init__.py +60 -0
  446. truthound/profiler/migration/base.py +345 -0
  447. truthound/profiler/migration/manager.py +444 -0
  448. truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
  449. truthound/profiler/ml/__init__.py +73 -0
  450. truthound/profiler/ml/base.py +244 -0
  451. truthound/profiler/ml/classifier.py +507 -0
  452. truthound/profiler/ml/feature_extraction.py +604 -0
  453. truthound/profiler/ml/pretrained.py +448 -0
  454. truthound/profiler/ml_inference.py +1276 -0
  455. truthound/profiler/native_patterns.py +815 -0
  456. truthound/profiler/observability.py +1184 -0
  457. truthound/profiler/process_timeout.py +1566 -0
  458. truthound/profiler/progress.py +568 -0
  459. truthound/profiler/progress_callbacks.py +1734 -0
  460. truthound/profiler/quality.py +1345 -0
  461. truthound/profiler/resilience.py +1180 -0
  462. truthound/profiler/sampled_matcher.py +794 -0
  463. truthound/profiler/sampling.py +1288 -0
  464. truthound/profiler/scheduling/__init__.py +82 -0
  465. truthound/profiler/scheduling/protocols.py +214 -0
  466. truthound/profiler/scheduling/scheduler.py +474 -0
  467. truthound/profiler/scheduling/storage.py +457 -0
  468. truthound/profiler/scheduling/triggers.py +449 -0
  469. truthound/profiler/schema.py +603 -0
  470. truthound/profiler/streaming.py +685 -0
  471. truthound/profiler/streaming_patterns.py +1354 -0
  472. truthound/profiler/suite_cli.py +625 -0
  473. truthound/profiler/suite_config.py +789 -0
  474. truthound/profiler/suite_export.py +1268 -0
  475. truthound/profiler/table_profiler.py +547 -0
  476. truthound/profiler/timeout.py +565 -0
  477. truthound/profiler/validation.py +1532 -0
  478. truthound/profiler/visualization/__init__.py +118 -0
  479. truthound/profiler/visualization/base.py +346 -0
  480. truthound/profiler/visualization/generator.py +1259 -0
  481. truthound/profiler/visualization/plotly_renderer.py +811 -0
  482. truthound/profiler/visualization/renderers.py +669 -0
  483. truthound/profiler/visualization/sections.py +540 -0
  484. truthound/profiler/visualization.py +2122 -0
  485. truthound/profiler/yaml_validation.py +1151 -0
  486. truthound/py.typed +0 -0
  487. truthound/ratelimit/__init__.py +248 -0
  488. truthound/ratelimit/algorithms.py +1108 -0
  489. truthound/ratelimit/core.py +573 -0
  490. truthound/ratelimit/integration.py +532 -0
  491. truthound/ratelimit/limiter.py +663 -0
  492. truthound/ratelimit/middleware.py +700 -0
  493. truthound/ratelimit/policy.py +792 -0
  494. truthound/ratelimit/storage.py +763 -0
  495. truthound/rbac/__init__.py +340 -0
  496. truthound/rbac/core.py +976 -0
  497. truthound/rbac/integration.py +760 -0
  498. truthound/rbac/manager.py +1052 -0
  499. truthound/rbac/middleware.py +842 -0
  500. truthound/rbac/policy.py +954 -0
  501. truthound/rbac/storage.py +878 -0
  502. truthound/realtime/__init__.py +141 -0
  503. truthound/realtime/adapters/__init__.py +43 -0
  504. truthound/realtime/adapters/base.py +533 -0
  505. truthound/realtime/adapters/kafka.py +487 -0
  506. truthound/realtime/adapters/kinesis.py +479 -0
  507. truthound/realtime/adapters/mock.py +243 -0
  508. truthound/realtime/base.py +553 -0
  509. truthound/realtime/factory.py +382 -0
  510. truthound/realtime/incremental.py +660 -0
  511. truthound/realtime/processing/__init__.py +67 -0
  512. truthound/realtime/processing/exactly_once.py +575 -0
  513. truthound/realtime/processing/state.py +547 -0
  514. truthound/realtime/processing/windows.py +647 -0
  515. truthound/realtime/protocols.py +569 -0
  516. truthound/realtime/streaming.py +605 -0
  517. truthound/realtime/testing/__init__.py +32 -0
  518. truthound/realtime/testing/containers.py +615 -0
  519. truthound/realtime/testing/fixtures.py +484 -0
  520. truthound/report.py +280 -0
  521. truthound/reporters/__init__.py +46 -0
  522. truthound/reporters/_protocols.py +30 -0
  523. truthound/reporters/base.py +324 -0
  524. truthound/reporters/ci/__init__.py +66 -0
  525. truthound/reporters/ci/azure.py +436 -0
  526. truthound/reporters/ci/base.py +509 -0
  527. truthound/reporters/ci/bitbucket.py +567 -0
  528. truthound/reporters/ci/circleci.py +547 -0
  529. truthound/reporters/ci/detection.py +364 -0
  530. truthound/reporters/ci/factory.py +182 -0
  531. truthound/reporters/ci/github.py +388 -0
  532. truthound/reporters/ci/gitlab.py +471 -0
  533. truthound/reporters/ci/jenkins.py +525 -0
  534. truthound/reporters/console_reporter.py +299 -0
  535. truthound/reporters/factory.py +211 -0
  536. truthound/reporters/html_reporter.py +524 -0
  537. truthound/reporters/json_reporter.py +256 -0
  538. truthound/reporters/markdown_reporter.py +280 -0
  539. truthound/reporters/sdk/__init__.py +174 -0
  540. truthound/reporters/sdk/builder.py +558 -0
  541. truthound/reporters/sdk/mixins.py +1150 -0
  542. truthound/reporters/sdk/schema.py +1493 -0
  543. truthound/reporters/sdk/templates.py +666 -0
  544. truthound/reporters/sdk/testing.py +968 -0
  545. truthound/scanners.py +170 -0
  546. truthound/scheduling/__init__.py +122 -0
  547. truthound/scheduling/cron.py +1136 -0
  548. truthound/scheduling/presets.py +212 -0
  549. truthound/schema.py +275 -0
  550. truthound/secrets/__init__.py +173 -0
  551. truthound/secrets/base.py +618 -0
  552. truthound/secrets/cloud.py +682 -0
  553. truthound/secrets/integration.py +507 -0
  554. truthound/secrets/manager.py +633 -0
  555. truthound/secrets/oidc/__init__.py +172 -0
  556. truthound/secrets/oidc/base.py +902 -0
  557. truthound/secrets/oidc/credential_provider.py +623 -0
  558. truthound/secrets/oidc/exchangers.py +1001 -0
  559. truthound/secrets/oidc/github/__init__.py +110 -0
  560. truthound/secrets/oidc/github/claims.py +718 -0
  561. truthound/secrets/oidc/github/enhanced_provider.py +693 -0
  562. truthound/secrets/oidc/github/trust_policy.py +742 -0
  563. truthound/secrets/oidc/github/verification.py +723 -0
  564. truthound/secrets/oidc/github/workflow.py +691 -0
  565. truthound/secrets/oidc/providers.py +825 -0
  566. truthound/secrets/providers.py +506 -0
  567. truthound/secrets/resolver.py +495 -0
  568. truthound/stores/__init__.py +177 -0
  569. truthound/stores/backends/__init__.py +18 -0
  570. truthound/stores/backends/_protocols.py +340 -0
  571. truthound/stores/backends/azure_blob.py +530 -0
  572. truthound/stores/backends/concurrent_filesystem.py +915 -0
  573. truthound/stores/backends/connection_pool.py +1365 -0
  574. truthound/stores/backends/database.py +743 -0
  575. truthound/stores/backends/filesystem.py +538 -0
  576. truthound/stores/backends/gcs.py +399 -0
  577. truthound/stores/backends/memory.py +354 -0
  578. truthound/stores/backends/s3.py +434 -0
  579. truthound/stores/backpressure/__init__.py +84 -0
  580. truthound/stores/backpressure/base.py +375 -0
  581. truthound/stores/backpressure/circuit_breaker.py +434 -0
  582. truthound/stores/backpressure/monitor.py +376 -0
  583. truthound/stores/backpressure/strategies.py +677 -0
  584. truthound/stores/base.py +551 -0
  585. truthound/stores/batching/__init__.py +65 -0
  586. truthound/stores/batching/base.py +305 -0
  587. truthound/stores/batching/buffer.py +370 -0
  588. truthound/stores/batching/store.py +248 -0
  589. truthound/stores/batching/writer.py +521 -0
  590. truthound/stores/caching/__init__.py +60 -0
  591. truthound/stores/caching/backends.py +684 -0
  592. truthound/stores/caching/base.py +356 -0
  593. truthound/stores/caching/store.py +305 -0
  594. truthound/stores/compression/__init__.py +193 -0
  595. truthound/stores/compression/adaptive.py +694 -0
  596. truthound/stores/compression/base.py +514 -0
  597. truthound/stores/compression/pipeline.py +868 -0
  598. truthound/stores/compression/providers.py +672 -0
  599. truthound/stores/compression/streaming.py +832 -0
  600. truthound/stores/concurrency/__init__.py +81 -0
  601. truthound/stores/concurrency/atomic.py +556 -0
  602. truthound/stores/concurrency/index.py +775 -0
  603. truthound/stores/concurrency/locks.py +576 -0
  604. truthound/stores/concurrency/manager.py +482 -0
  605. truthound/stores/encryption/__init__.py +297 -0
  606. truthound/stores/encryption/base.py +952 -0
  607. truthound/stores/encryption/keys.py +1191 -0
  608. truthound/stores/encryption/pipeline.py +903 -0
  609. truthound/stores/encryption/providers.py +953 -0
  610. truthound/stores/encryption/streaming.py +950 -0
  611. truthound/stores/expectations.py +227 -0
  612. truthound/stores/factory.py +246 -0
  613. truthound/stores/migration/__init__.py +75 -0
  614. truthound/stores/migration/base.py +480 -0
  615. truthound/stores/migration/manager.py +347 -0
  616. truthound/stores/migration/registry.py +382 -0
  617. truthound/stores/migration/store.py +559 -0
  618. truthound/stores/observability/__init__.py +106 -0
  619. truthound/stores/observability/audit.py +718 -0
  620. truthound/stores/observability/config.py +270 -0
  621. truthound/stores/observability/factory.py +208 -0
  622. truthound/stores/observability/metrics.py +636 -0
  623. truthound/stores/observability/protocols.py +410 -0
  624. truthound/stores/observability/store.py +570 -0
  625. truthound/stores/observability/tracing.py +784 -0
  626. truthound/stores/replication/__init__.py +76 -0
  627. truthound/stores/replication/base.py +260 -0
  628. truthound/stores/replication/monitor.py +269 -0
  629. truthound/stores/replication/store.py +439 -0
  630. truthound/stores/replication/syncer.py +391 -0
  631. truthound/stores/results.py +359 -0
  632. truthound/stores/retention/__init__.py +77 -0
  633. truthound/stores/retention/base.py +378 -0
  634. truthound/stores/retention/policies.py +621 -0
  635. truthound/stores/retention/scheduler.py +279 -0
  636. truthound/stores/retention/store.py +526 -0
  637. truthound/stores/streaming/__init__.py +138 -0
  638. truthound/stores/streaming/base.py +801 -0
  639. truthound/stores/streaming/database.py +984 -0
  640. truthound/stores/streaming/filesystem.py +719 -0
  641. truthound/stores/streaming/reader.py +629 -0
  642. truthound/stores/streaming/s3.py +843 -0
  643. truthound/stores/streaming/writer.py +790 -0
  644. truthound/stores/tiering/__init__.py +108 -0
  645. truthound/stores/tiering/base.py +462 -0
  646. truthound/stores/tiering/manager.py +249 -0
  647. truthound/stores/tiering/policies.py +692 -0
  648. truthound/stores/tiering/store.py +526 -0
  649. truthound/stores/versioning/__init__.py +56 -0
  650. truthound/stores/versioning/base.py +376 -0
  651. truthound/stores/versioning/store.py +660 -0
  652. truthound/stores/versioning/strategies.py +353 -0
  653. truthound/types.py +56 -0
  654. truthound/validators/__init__.py +774 -0
  655. truthound/validators/aggregate/__init__.py +27 -0
  656. truthound/validators/aggregate/central.py +116 -0
  657. truthound/validators/aggregate/extremes.py +116 -0
  658. truthound/validators/aggregate/spread.py +118 -0
  659. truthound/validators/aggregate/sum.py +64 -0
  660. truthound/validators/aggregate/type.py +78 -0
  661. truthound/validators/anomaly/__init__.py +93 -0
  662. truthound/validators/anomaly/base.py +431 -0
  663. truthound/validators/anomaly/ml_based.py +1190 -0
  664. truthound/validators/anomaly/multivariate.py +647 -0
  665. truthound/validators/anomaly/statistical.py +599 -0
  666. truthound/validators/base.py +1089 -0
  667. truthound/validators/business_rule/__init__.py +46 -0
  668. truthound/validators/business_rule/base.py +147 -0
  669. truthound/validators/business_rule/checksum.py +509 -0
  670. truthound/validators/business_rule/financial.py +526 -0
  671. truthound/validators/cache.py +733 -0
  672. truthound/validators/completeness/__init__.py +39 -0
  673. truthound/validators/completeness/conditional.py +73 -0
  674. truthound/validators/completeness/default.py +98 -0
  675. truthound/validators/completeness/empty.py +103 -0
  676. truthound/validators/completeness/nan.py +337 -0
  677. truthound/validators/completeness/null.py +152 -0
  678. truthound/validators/cross_table/__init__.py +17 -0
  679. truthound/validators/cross_table/aggregate.py +333 -0
  680. truthound/validators/cross_table/row_count.py +122 -0
  681. truthound/validators/datetime/__init__.py +29 -0
  682. truthound/validators/datetime/format.py +78 -0
  683. truthound/validators/datetime/freshness.py +269 -0
  684. truthound/validators/datetime/order.py +73 -0
  685. truthound/validators/datetime/parseable.py +185 -0
  686. truthound/validators/datetime/range.py +202 -0
  687. truthound/validators/datetime/timezone.py +69 -0
  688. truthound/validators/distribution/__init__.py +49 -0
  689. truthound/validators/distribution/distribution.py +128 -0
  690. truthound/validators/distribution/monotonic.py +119 -0
  691. truthound/validators/distribution/outlier.py +178 -0
  692. truthound/validators/distribution/quantile.py +80 -0
  693. truthound/validators/distribution/range.py +254 -0
  694. truthound/validators/distribution/set.py +125 -0
  695. truthound/validators/distribution/statistical.py +459 -0
  696. truthound/validators/drift/__init__.py +79 -0
  697. truthound/validators/drift/base.py +427 -0
  698. truthound/validators/drift/multi_feature.py +401 -0
  699. truthound/validators/drift/numeric.py +395 -0
  700. truthound/validators/drift/psi.py +446 -0
  701. truthound/validators/drift/statistical.py +510 -0
  702. truthound/validators/enterprise.py +1658 -0
  703. truthound/validators/geospatial/__init__.py +80 -0
  704. truthound/validators/geospatial/base.py +97 -0
  705. truthound/validators/geospatial/boundary.py +238 -0
  706. truthound/validators/geospatial/coordinate.py +351 -0
  707. truthound/validators/geospatial/distance.py +399 -0
  708. truthound/validators/geospatial/polygon.py +665 -0
  709. truthound/validators/i18n/__init__.py +308 -0
  710. truthound/validators/i18n/bidi.py +571 -0
  711. truthound/validators/i18n/catalogs.py +570 -0
  712. truthound/validators/i18n/dialects.py +763 -0
  713. truthound/validators/i18n/extended_catalogs.py +549 -0
  714. truthound/validators/i18n/formatting.py +1434 -0
  715. truthound/validators/i18n/loader.py +1020 -0
  716. truthound/validators/i18n/messages.py +521 -0
  717. truthound/validators/i18n/plural.py +683 -0
  718. truthound/validators/i18n/protocols.py +855 -0
  719. truthound/validators/i18n/tms.py +1162 -0
  720. truthound/validators/localization/__init__.py +53 -0
  721. truthound/validators/localization/base.py +122 -0
  722. truthound/validators/localization/chinese.py +362 -0
  723. truthound/validators/localization/japanese.py +275 -0
  724. truthound/validators/localization/korean.py +524 -0
  725. truthound/validators/memory/__init__.py +94 -0
  726. truthound/validators/memory/approximate_knn.py +506 -0
  727. truthound/validators/memory/base.py +547 -0
  728. truthound/validators/memory/sgd_online.py +719 -0
  729. truthound/validators/memory/streaming_ecdf.py +753 -0
  730. truthound/validators/ml_feature/__init__.py +54 -0
  731. truthound/validators/ml_feature/base.py +249 -0
  732. truthound/validators/ml_feature/correlation.py +299 -0
  733. truthound/validators/ml_feature/leakage.py +344 -0
  734. truthound/validators/ml_feature/null_impact.py +270 -0
  735. truthound/validators/ml_feature/scale.py +264 -0
  736. truthound/validators/multi_column/__init__.py +89 -0
  737. truthound/validators/multi_column/arithmetic.py +284 -0
  738. truthound/validators/multi_column/base.py +231 -0
  739. truthound/validators/multi_column/comparison.py +273 -0
  740. truthound/validators/multi_column/consistency.py +312 -0
  741. truthound/validators/multi_column/statistical.py +299 -0
  742. truthound/validators/optimization/__init__.py +164 -0
  743. truthound/validators/optimization/aggregation.py +563 -0
  744. truthound/validators/optimization/covariance.py +556 -0
  745. truthound/validators/optimization/geo.py +626 -0
  746. truthound/validators/optimization/graph.py +587 -0
  747. truthound/validators/optimization/orchestrator.py +970 -0
  748. truthound/validators/optimization/profiling.py +1312 -0
  749. truthound/validators/privacy/__init__.py +223 -0
  750. truthound/validators/privacy/base.py +635 -0
  751. truthound/validators/privacy/ccpa.py +670 -0
  752. truthound/validators/privacy/gdpr.py +728 -0
  753. truthound/validators/privacy/global_patterns.py +604 -0
  754. truthound/validators/privacy/plugins.py +867 -0
  755. truthound/validators/profiling/__init__.py +52 -0
  756. truthound/validators/profiling/base.py +175 -0
  757. truthound/validators/profiling/cardinality.py +312 -0
  758. truthound/validators/profiling/entropy.py +391 -0
  759. truthound/validators/profiling/frequency.py +455 -0
  760. truthound/validators/pushdown_support.py +660 -0
  761. truthound/validators/query/__init__.py +91 -0
  762. truthound/validators/query/aggregate.py +346 -0
  763. truthound/validators/query/base.py +246 -0
  764. truthound/validators/query/column.py +249 -0
  765. truthound/validators/query/expression.py +274 -0
  766. truthound/validators/query/result.py +323 -0
  767. truthound/validators/query/row_count.py +264 -0
  768. truthound/validators/referential/__init__.py +80 -0
  769. truthound/validators/referential/base.py +395 -0
  770. truthound/validators/referential/cascade.py +391 -0
  771. truthound/validators/referential/circular.py +563 -0
  772. truthound/validators/referential/foreign_key.py +624 -0
  773. truthound/validators/referential/orphan.py +485 -0
  774. truthound/validators/registry.py +112 -0
  775. truthound/validators/schema/__init__.py +41 -0
  776. truthound/validators/schema/column_count.py +142 -0
  777. truthound/validators/schema/column_exists.py +80 -0
  778. truthound/validators/schema/column_order.py +82 -0
  779. truthound/validators/schema/column_pair.py +85 -0
  780. truthound/validators/schema/column_pair_set.py +195 -0
  781. truthound/validators/schema/column_type.py +94 -0
  782. truthound/validators/schema/multi_column.py +53 -0
  783. truthound/validators/schema/multi_column_aggregate.py +175 -0
  784. truthound/validators/schema/referential.py +274 -0
  785. truthound/validators/schema/table_schema.py +91 -0
  786. truthound/validators/schema_validator.py +219 -0
  787. truthound/validators/sdk/__init__.py +250 -0
  788. truthound/validators/sdk/builder.py +680 -0
  789. truthound/validators/sdk/decorators.py +474 -0
  790. truthound/validators/sdk/enterprise/__init__.py +211 -0
  791. truthound/validators/sdk/enterprise/docs.py +725 -0
  792. truthound/validators/sdk/enterprise/fuzzing.py +659 -0
  793. truthound/validators/sdk/enterprise/licensing.py +709 -0
  794. truthound/validators/sdk/enterprise/manager.py +543 -0
  795. truthound/validators/sdk/enterprise/resources.py +628 -0
  796. truthound/validators/sdk/enterprise/sandbox.py +766 -0
  797. truthound/validators/sdk/enterprise/signing.py +603 -0
  798. truthound/validators/sdk/enterprise/templates.py +865 -0
  799. truthound/validators/sdk/enterprise/versioning.py +659 -0
  800. truthound/validators/sdk/templates.py +757 -0
  801. truthound/validators/sdk/testing.py +807 -0
  802. truthound/validators/security/__init__.py +181 -0
  803. truthound/validators/security/redos/__init__.py +182 -0
  804. truthound/validators/security/redos/core.py +861 -0
  805. truthound/validators/security/redos/cpu_monitor.py +593 -0
  806. truthound/validators/security/redos/cve_database.py +791 -0
  807. truthound/validators/security/redos/ml/__init__.py +155 -0
  808. truthound/validators/security/redos/ml/base.py +785 -0
  809. truthound/validators/security/redos/ml/datasets.py +618 -0
  810. truthound/validators/security/redos/ml/features.py +359 -0
  811. truthound/validators/security/redos/ml/models.py +1000 -0
  812. truthound/validators/security/redos/ml/predictor.py +507 -0
  813. truthound/validators/security/redos/ml/storage.py +632 -0
  814. truthound/validators/security/redos/ml/training.py +571 -0
  815. truthound/validators/security/redos/ml_analyzer.py +937 -0
  816. truthound/validators/security/redos/optimizer.py +674 -0
  817. truthound/validators/security/redos/profiler.py +682 -0
  818. truthound/validators/security/redos/re2_engine.py +709 -0
  819. truthound/validators/security/redos.py +886 -0
  820. truthound/validators/security/sql_security.py +1247 -0
  821. truthound/validators/streaming/__init__.py +126 -0
  822. truthound/validators/streaming/base.py +292 -0
  823. truthound/validators/streaming/completeness.py +210 -0
  824. truthound/validators/streaming/mixin.py +575 -0
  825. truthound/validators/streaming/range.py +308 -0
  826. truthound/validators/streaming/sources.py +846 -0
  827. truthound/validators/string/__init__.py +57 -0
  828. truthound/validators/string/casing.py +158 -0
  829. truthound/validators/string/charset.py +96 -0
  830. truthound/validators/string/format.py +501 -0
  831. truthound/validators/string/json.py +77 -0
  832. truthound/validators/string/json_schema.py +184 -0
  833. truthound/validators/string/length.py +104 -0
  834. truthound/validators/string/like_pattern.py +237 -0
  835. truthound/validators/string/regex.py +202 -0
  836. truthound/validators/string/regex_extended.py +435 -0
  837. truthound/validators/table/__init__.py +88 -0
  838. truthound/validators/table/base.py +78 -0
  839. truthound/validators/table/column_count.py +198 -0
  840. truthound/validators/table/freshness.py +362 -0
  841. truthound/validators/table/row_count.py +251 -0
  842. truthound/validators/table/schema.py +333 -0
  843. truthound/validators/table/size.py +285 -0
  844. truthound/validators/timeout/__init__.py +102 -0
  845. truthound/validators/timeout/advanced/__init__.py +247 -0
  846. truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
  847. truthound/validators/timeout/advanced/prediction.py +773 -0
  848. truthound/validators/timeout/advanced/priority.py +618 -0
  849. truthound/validators/timeout/advanced/redis_backend.py +770 -0
  850. truthound/validators/timeout/advanced/retry.py +721 -0
  851. truthound/validators/timeout/advanced/sampling.py +788 -0
  852. truthound/validators/timeout/advanced/sla.py +661 -0
  853. truthound/validators/timeout/advanced/telemetry.py +804 -0
  854. truthound/validators/timeout/cascade.py +477 -0
  855. truthound/validators/timeout/deadline.py +657 -0
  856. truthound/validators/timeout/degradation.py +525 -0
  857. truthound/validators/timeout/distributed.py +597 -0
  858. truthound/validators/timeseries/__init__.py +89 -0
  859. truthound/validators/timeseries/base.py +326 -0
  860. truthound/validators/timeseries/completeness.py +617 -0
  861. truthound/validators/timeseries/gap.py +485 -0
  862. truthound/validators/timeseries/monotonic.py +310 -0
  863. truthound/validators/timeseries/seasonality.py +422 -0
  864. truthound/validators/timeseries/trend.py +510 -0
  865. truthound/validators/uniqueness/__init__.py +59 -0
  866. truthound/validators/uniqueness/approximate.py +475 -0
  867. truthound/validators/uniqueness/distinct_values.py +253 -0
  868. truthound/validators/uniqueness/duplicate.py +118 -0
  869. truthound/validators/uniqueness/primary_key.py +140 -0
  870. truthound/validators/uniqueness/unique.py +191 -0
  871. truthound/validators/uniqueness/within_record.py +599 -0
  872. truthound/validators/utils.py +756 -0
  873. truthound-1.0.8.dist-info/METADATA +474 -0
  874. truthound-1.0.8.dist-info/RECORD +877 -0
  875. truthound-1.0.8.dist-info/WHEEL +4 -0
  876. truthound-1.0.8.dist-info/entry_points.txt +2 -0
  877. 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
+ )