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,1493 @@
1
+ """Reporter output validation and schema support.
2
+
3
+ This module provides schema validation for reporter outputs to ensure
4
+ consistent, well-formed reports across different reporter implementations.
5
+
6
+ Example:
7
+ >>> from truthound.reporters.sdk.schema import (
8
+ ... ReportSchema,
9
+ ... JSONSchema,
10
+ ... validate_output,
11
+ ... register_schema,
12
+ ... )
13
+ >>>
14
+ >>> # Define a schema for your reporter
15
+ >>> schema = JSONSchema({
16
+ ... "type": "object",
17
+ ... "properties": {
18
+ ... "summary": {"type": "object"},
19
+ ... "results": {"type": "array"},
20
+ ... },
21
+ ... "required": ["summary", "results"],
22
+ ... })
23
+ >>>
24
+ >>> # Register for automatic validation
25
+ >>> register_schema("my_reporter", schema)
26
+ >>>
27
+ >>> # Validate output
28
+ >>> result = validate_output(output, schema)
29
+ >>> if not result.valid:
30
+ ... print(f"Validation errors: {result.errors}")
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import re
37
+ from abc import ABC, abstractmethod
38
+ from dataclasses import dataclass, field
39
+ from enum import Enum
40
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
41
+ from xml.etree import ElementTree
42
+
43
+ __all__ = [
44
+ # Core classes
45
+ "ReportSchema",
46
+ "JSONSchema",
47
+ "XMLSchema",
48
+ "CSVSchema",
49
+ "TextSchema",
50
+ # Validation
51
+ "ValidationResult",
52
+ "ValidationError",
53
+ "SchemaError",
54
+ # Functions
55
+ "validate_output",
56
+ "register_schema",
57
+ "get_schema",
58
+ "unregister_schema",
59
+ # Decorators
60
+ "validate_reporter_output",
61
+ # Utilities
62
+ "infer_schema",
63
+ "merge_schemas",
64
+ ]
65
+
66
+
67
+ class SchemaError(Exception):
68
+ """Exception raised for schema-related errors."""
69
+
70
+ pass
71
+
72
+
73
+ class ValidationError(Exception):
74
+ """Exception raised when validation fails."""
75
+
76
+ def __init__(
77
+ self,
78
+ message: str,
79
+ path: Optional[str] = None,
80
+ value: Any = None,
81
+ expected: Optional[str] = None,
82
+ ) -> None:
83
+ self.message = message
84
+ self.path = path
85
+ self.value = value
86
+ self.expected = expected
87
+ super().__init__(self._format_message())
88
+
89
+ def _format_message(self) -> str:
90
+ parts = [self.message]
91
+ if self.path:
92
+ parts.append(f"at path '{self.path}'")
93
+ if self.expected:
94
+ parts.append(f"(expected: {self.expected})")
95
+ return " ".join(parts)
96
+
97
+
98
+ @dataclass
99
+ class ValidationResult:
100
+ """Result of schema validation."""
101
+
102
+ valid: bool
103
+ errors: List[ValidationError] = field(default_factory=list)
104
+ warnings: List[str] = field(default_factory=list)
105
+ schema_name: Optional[str] = None
106
+ checked_at: Optional[str] = None
107
+
108
+ def raise_if_invalid(self) -> None:
109
+ """Raise SchemaError if validation failed."""
110
+ if not self.valid:
111
+ error_messages = [str(e) for e in self.errors]
112
+ raise SchemaError(
113
+ f"Validation failed with {len(self.errors)} error(s): "
114
+ + "; ".join(error_messages)
115
+ )
116
+
117
+ def to_dict(self) -> Dict[str, Any]:
118
+ """Convert to dictionary representation."""
119
+ return {
120
+ "valid": self.valid,
121
+ "errors": [
122
+ {
123
+ "message": e.message,
124
+ "path": e.path,
125
+ "value": repr(e.value) if e.value is not None else None,
126
+ "expected": e.expected,
127
+ }
128
+ for e in self.errors
129
+ ],
130
+ "warnings": self.warnings,
131
+ "schema_name": self.schema_name,
132
+ "checked_at": self.checked_at,
133
+ }
134
+
135
+
136
+ class ReportSchema(ABC):
137
+ """Base class for report schemas.
138
+
139
+ Subclass this to create custom schema validators for your
140
+ reporter output format.
141
+
142
+ Example:
143
+ >>> class MyCustomSchema(ReportSchema):
144
+ ... def validate(self, output: Any) -> ValidationResult:
145
+ ... errors = []
146
+ ... if not isinstance(output, dict):
147
+ ... errors.append(ValidationError("Expected dictionary"))
148
+ ... return ValidationResult(valid=len(errors) == 0, errors=errors)
149
+ ...
150
+ ... def to_dict(self) -> Dict[str, Any]:
151
+ ... return {"type": "custom"}
152
+ """
153
+
154
+ def __init__(self, name: Optional[str] = None) -> None:
155
+ self.name = name or self.__class__.__name__
156
+
157
+ @abstractmethod
158
+ def validate(self, output: Any) -> ValidationResult:
159
+ """Validate output against this schema.
160
+
161
+ Args:
162
+ output: The output to validate.
163
+
164
+ Returns:
165
+ ValidationResult with validation status and any errors.
166
+ """
167
+ pass
168
+
169
+ @abstractmethod
170
+ def to_dict(self) -> Dict[str, Any]:
171
+ """Convert schema to dictionary representation."""
172
+ pass
173
+
174
+ def __repr__(self) -> str:
175
+ return f"{self.__class__.__name__}(name={self.name!r})"
176
+
177
+
178
+ class JSONSchema(ReportSchema):
179
+ """JSON Schema validator for reporter output.
180
+
181
+ Supports a subset of JSON Schema draft-07 for validating
182
+ JSON/dictionary output from reporters.
183
+
184
+ Example:
185
+ >>> schema = JSONSchema({
186
+ ... "type": "object",
187
+ ... "properties": {
188
+ ... "name": {"type": "string"},
189
+ ... "count": {"type": "integer", "minimum": 0},
190
+ ... },
191
+ ... "required": ["name"],
192
+ ... })
193
+ >>> result = schema.validate({"name": "test", "count": 5})
194
+ >>> print(result.valid) # True
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ schema: Dict[str, Any],
200
+ name: Optional[str] = None,
201
+ strict: bool = False,
202
+ ) -> None:
203
+ """Initialize JSON Schema validator.
204
+
205
+ Args:
206
+ schema: JSON Schema definition.
207
+ name: Optional schema name.
208
+ strict: If True, disallow additional properties by default.
209
+ """
210
+ super().__init__(name)
211
+ self.schema = schema
212
+ self.strict = strict
213
+ self._type_validators: Dict[str, Callable] = {
214
+ "string": lambda v: isinstance(v, str),
215
+ "integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
216
+ "number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
217
+ "boolean": lambda v: isinstance(v, bool),
218
+ "array": lambda v: isinstance(v, list),
219
+ "object": lambda v: isinstance(v, dict),
220
+ "null": lambda v: v is None,
221
+ }
222
+
223
+ def validate(self, output: Any) -> ValidationResult:
224
+ """Validate output against JSON Schema."""
225
+ from datetime import datetime
226
+
227
+ errors: List[ValidationError] = []
228
+ warnings: List[str] = []
229
+
230
+ self._validate_value(output, self.schema, "", errors, warnings)
231
+
232
+ return ValidationResult(
233
+ valid=len(errors) == 0,
234
+ errors=errors,
235
+ warnings=warnings,
236
+ schema_name=self.name,
237
+ checked_at=datetime.now().isoformat(),
238
+ )
239
+
240
+ def _validate_value(
241
+ self,
242
+ value: Any,
243
+ schema: Dict[str, Any],
244
+ path: str,
245
+ errors: List[ValidationError],
246
+ warnings: List[str],
247
+ ) -> None:
248
+ """Recursively validate a value against schema."""
249
+ # Handle type validation
250
+ if "type" in schema:
251
+ expected_types = schema["type"]
252
+ if isinstance(expected_types, str):
253
+ expected_types = [expected_types]
254
+
255
+ type_valid = any(
256
+ self._type_validators.get(t, lambda v: False)(value)
257
+ for t in expected_types
258
+ )
259
+
260
+ if not type_valid:
261
+ errors.append(
262
+ ValidationError(
263
+ f"Invalid type: got {type(value).__name__}",
264
+ path=path or "$",
265
+ value=value,
266
+ expected=", ".join(expected_types),
267
+ )
268
+ )
269
+ return
270
+
271
+ # Handle enum validation
272
+ if "enum" in schema:
273
+ if value not in schema["enum"]:
274
+ errors.append(
275
+ ValidationError(
276
+ f"Value not in enum",
277
+ path=path or "$",
278
+ value=value,
279
+ expected=str(schema["enum"]),
280
+ )
281
+ )
282
+
283
+ # Handle const validation
284
+ if "const" in schema:
285
+ if value != schema["const"]:
286
+ errors.append(
287
+ ValidationError(
288
+ f"Value does not match const",
289
+ path=path or "$",
290
+ value=value,
291
+ expected=repr(schema["const"]),
292
+ )
293
+ )
294
+
295
+ # Handle string-specific validations
296
+ if isinstance(value, str):
297
+ self._validate_string(value, schema, path, errors)
298
+
299
+ # Handle number-specific validations
300
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
301
+ self._validate_number(value, schema, path, errors)
302
+
303
+ # Handle array validations
304
+ if isinstance(value, list):
305
+ self._validate_array(value, schema, path, errors, warnings)
306
+
307
+ # Handle object validations
308
+ if isinstance(value, dict):
309
+ self._validate_object(value, schema, path, errors, warnings)
310
+
311
+ def _validate_string(
312
+ self,
313
+ value: str,
314
+ schema: Dict[str, Any],
315
+ path: str,
316
+ errors: List[ValidationError],
317
+ ) -> None:
318
+ """Validate string-specific constraints."""
319
+ if "minLength" in schema and len(value) < schema["minLength"]:
320
+ errors.append(
321
+ ValidationError(
322
+ f"String too short (length: {len(value)})",
323
+ path=path,
324
+ value=value,
325
+ expected=f"minLength: {schema['minLength']}",
326
+ )
327
+ )
328
+
329
+ if "maxLength" in schema and len(value) > schema["maxLength"]:
330
+ errors.append(
331
+ ValidationError(
332
+ f"String too long (length: {len(value)})",
333
+ path=path,
334
+ value=value,
335
+ expected=f"maxLength: {schema['maxLength']}",
336
+ )
337
+ )
338
+
339
+ if "pattern" in schema:
340
+ if not re.match(schema["pattern"], value):
341
+ errors.append(
342
+ ValidationError(
343
+ f"String does not match pattern",
344
+ path=path,
345
+ value=value,
346
+ expected=f"pattern: {schema['pattern']}",
347
+ )
348
+ )
349
+
350
+ if "format" in schema:
351
+ self._validate_format(value, schema["format"], path, errors)
352
+
353
+ def _validate_format(
354
+ self,
355
+ value: str,
356
+ format_type: str,
357
+ path: str,
358
+ errors: List[ValidationError],
359
+ ) -> None:
360
+ """Validate string format."""
361
+ format_patterns = {
362
+ "email": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
363
+ "uri": r"^https?://[^\s]+$",
364
+ "date": r"^\d{4}-\d{2}-\d{2}$",
365
+ "time": r"^\d{2}:\d{2}:\d{2}$",
366
+ "date-time": r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}",
367
+ "uuid": r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
368
+ "ipv4": r"^(\d{1,3}\.){3}\d{1,3}$",
369
+ }
370
+
371
+ if format_type in format_patterns:
372
+ if not re.match(format_patterns[format_type], value):
373
+ errors.append(
374
+ ValidationError(
375
+ f"String does not match format",
376
+ path=path,
377
+ value=value,
378
+ expected=f"format: {format_type}",
379
+ )
380
+ )
381
+
382
+ def _validate_number(
383
+ self,
384
+ value: Union[int, float],
385
+ schema: Dict[str, Any],
386
+ path: str,
387
+ errors: List[ValidationError],
388
+ ) -> None:
389
+ """Validate number-specific constraints."""
390
+ if "minimum" in schema and value < schema["minimum"]:
391
+ errors.append(
392
+ ValidationError(
393
+ f"Number below minimum",
394
+ path=path,
395
+ value=value,
396
+ expected=f"minimum: {schema['minimum']}",
397
+ )
398
+ )
399
+
400
+ if "maximum" in schema and value > schema["maximum"]:
401
+ errors.append(
402
+ ValidationError(
403
+ f"Number above maximum",
404
+ path=path,
405
+ value=value,
406
+ expected=f"maximum: {schema['maximum']}",
407
+ )
408
+ )
409
+
410
+ if "exclusiveMinimum" in schema and value <= schema["exclusiveMinimum"]:
411
+ errors.append(
412
+ ValidationError(
413
+ f"Number not greater than exclusive minimum",
414
+ path=path,
415
+ value=value,
416
+ expected=f"exclusiveMinimum: {schema['exclusiveMinimum']}",
417
+ )
418
+ )
419
+
420
+ if "exclusiveMaximum" in schema and value >= schema["exclusiveMaximum"]:
421
+ errors.append(
422
+ ValidationError(
423
+ f"Number not less than exclusive maximum",
424
+ path=path,
425
+ value=value,
426
+ expected=f"exclusiveMaximum: {schema['exclusiveMaximum']}",
427
+ )
428
+ )
429
+
430
+ if "multipleOf" in schema and value % schema["multipleOf"] != 0:
431
+ errors.append(
432
+ ValidationError(
433
+ f"Number is not a multiple",
434
+ path=path,
435
+ value=value,
436
+ expected=f"multipleOf: {schema['multipleOf']}",
437
+ )
438
+ )
439
+
440
+ def _validate_array(
441
+ self,
442
+ value: List[Any],
443
+ schema: Dict[str, Any],
444
+ path: str,
445
+ errors: List[ValidationError],
446
+ warnings: List[str],
447
+ ) -> None:
448
+ """Validate array-specific constraints."""
449
+ if "minItems" in schema and len(value) < schema["minItems"]:
450
+ errors.append(
451
+ ValidationError(
452
+ f"Array too short (length: {len(value)})",
453
+ path=path,
454
+ value=f"[{len(value)} items]",
455
+ expected=f"minItems: {schema['minItems']}",
456
+ )
457
+ )
458
+
459
+ if "maxItems" in schema and len(value) > schema["maxItems"]:
460
+ errors.append(
461
+ ValidationError(
462
+ f"Array too long (length: {len(value)})",
463
+ path=path,
464
+ value=f"[{len(value)} items]",
465
+ expected=f"maxItems: {schema['maxItems']}",
466
+ )
467
+ )
468
+
469
+ if "uniqueItems" in schema and schema["uniqueItems"]:
470
+ try:
471
+ # Check for duplicates using JSON serialization for hashability
472
+ seen = set()
473
+ for item in value:
474
+ key = json.dumps(item, sort_keys=True, default=str)
475
+ if key in seen:
476
+ errors.append(
477
+ ValidationError(
478
+ f"Array contains duplicate items",
479
+ path=path,
480
+ value=item,
481
+ )
482
+ )
483
+ break
484
+ seen.add(key)
485
+ except Exception:
486
+ warnings.append(f"Could not check uniqueItems at {path}")
487
+
488
+ # Validate items
489
+ if "items" in schema:
490
+ item_schema = schema["items"]
491
+ for i, item in enumerate(value):
492
+ item_path = f"{path}[{i}]"
493
+ self._validate_value(item, item_schema, item_path, errors, warnings)
494
+
495
+ def _validate_object(
496
+ self,
497
+ value: Dict[str, Any],
498
+ schema: Dict[str, Any],
499
+ path: str,
500
+ errors: List[ValidationError],
501
+ warnings: List[str],
502
+ ) -> None:
503
+ """Validate object-specific constraints."""
504
+ # Check required properties
505
+ required = schema.get("required", [])
506
+ for prop in required:
507
+ if prop not in value:
508
+ errors.append(
509
+ ValidationError(
510
+ f"Missing required property: {prop}",
511
+ path=path or "$",
512
+ expected=f"property '{prop}'",
513
+ )
514
+ )
515
+
516
+ # Validate properties
517
+ properties = schema.get("properties", {})
518
+ additional_properties = schema.get(
519
+ "additionalProperties", not self.strict
520
+ )
521
+
522
+ for key, prop_value in value.items():
523
+ prop_path = f"{path}.{key}" if path else key
524
+
525
+ if key in properties:
526
+ self._validate_value(
527
+ prop_value, properties[key], prop_path, errors, warnings
528
+ )
529
+ elif not additional_properties:
530
+ errors.append(
531
+ ValidationError(
532
+ f"Additional property not allowed: {key}",
533
+ path=prop_path,
534
+ value=prop_value,
535
+ )
536
+ )
537
+ elif isinstance(additional_properties, dict):
538
+ # additionalProperties can be a schema
539
+ self._validate_value(
540
+ prop_value, additional_properties, prop_path, errors, warnings
541
+ )
542
+
543
+ # Check property count
544
+ if "minProperties" in schema and len(value) < schema["minProperties"]:
545
+ errors.append(
546
+ ValidationError(
547
+ f"Object has too few properties",
548
+ path=path or "$",
549
+ value=f"{len(value)} properties",
550
+ expected=f"minProperties: {schema['minProperties']}",
551
+ )
552
+ )
553
+
554
+ if "maxProperties" in schema and len(value) > schema["maxProperties"]:
555
+ errors.append(
556
+ ValidationError(
557
+ f"Object has too many properties",
558
+ path=path or "$",
559
+ value=f"{len(value)} properties",
560
+ expected=f"maxProperties: {schema['maxProperties']}",
561
+ )
562
+ )
563
+
564
+ def to_dict(self) -> Dict[str, Any]:
565
+ """Convert to dictionary representation."""
566
+ return {
567
+ "type": "json_schema",
568
+ "name": self.name,
569
+ "strict": self.strict,
570
+ "schema": self.schema,
571
+ }
572
+
573
+
574
+ class XMLSchema(ReportSchema):
575
+ """XML structure validator for reporter output.
576
+
577
+ Validates XML output structure including elements, attributes,
578
+ and text content.
579
+
580
+ Example:
581
+ >>> schema = XMLSchema(
582
+ ... root_element="report",
583
+ ... required_elements=["summary", "results"],
584
+ ... required_attributes={"report": ["version"]},
585
+ ... )
586
+ >>> result = schema.validate(xml_string)
587
+ """
588
+
589
+ def __init__(
590
+ self,
591
+ root_element: str,
592
+ required_elements: Optional[List[str]] = None,
593
+ required_attributes: Optional[Dict[str, List[str]]] = None,
594
+ element_schemas: Optional[Dict[str, Dict[str, Any]]] = None,
595
+ name: Optional[str] = None,
596
+ ) -> None:
597
+ """Initialize XML Schema validator.
598
+
599
+ Args:
600
+ root_element: Expected root element name.
601
+ required_elements: List of required element names.
602
+ required_attributes: Dict mapping element names to required attributes.
603
+ element_schemas: Dict mapping element names to validation rules.
604
+ name: Optional schema name.
605
+ """
606
+ super().__init__(name)
607
+ self.root_element = root_element
608
+ self.required_elements = required_elements or []
609
+ self.required_attributes = required_attributes or {}
610
+ self.element_schemas = element_schemas or {}
611
+
612
+ def validate(self, output: Union[str, bytes, ElementTree.Element]) -> ValidationResult:
613
+ """Validate XML output."""
614
+ from datetime import datetime
615
+
616
+ errors: List[ValidationError] = []
617
+ warnings: List[str] = []
618
+
619
+ # Parse XML if string/bytes
620
+ try:
621
+ if isinstance(output, (str, bytes)):
622
+ root = ElementTree.fromstring(output)
623
+ elif isinstance(output, ElementTree.Element):
624
+ root = output
625
+ else:
626
+ errors.append(
627
+ ValidationError(
628
+ f"Expected XML string, bytes, or Element, got {type(output).__name__}",
629
+ path="$",
630
+ )
631
+ )
632
+ return ValidationResult(
633
+ valid=False,
634
+ errors=errors,
635
+ schema_name=self.name,
636
+ checked_at=datetime.now().isoformat(),
637
+ )
638
+ except ElementTree.ParseError as e:
639
+ errors.append(
640
+ ValidationError(
641
+ f"XML parse error: {e}",
642
+ path="$",
643
+ )
644
+ )
645
+ return ValidationResult(
646
+ valid=False,
647
+ errors=errors,
648
+ schema_name=self.name,
649
+ checked_at=datetime.now().isoformat(),
650
+ )
651
+
652
+ # Validate root element
653
+ if root.tag != self.root_element:
654
+ errors.append(
655
+ ValidationError(
656
+ f"Invalid root element",
657
+ path="$",
658
+ value=root.tag,
659
+ expected=self.root_element,
660
+ )
661
+ )
662
+
663
+ # Validate required elements
664
+ found_elements = {elem.tag for elem in root.iter()}
665
+ for required in self.required_elements:
666
+ if required not in found_elements:
667
+ errors.append(
668
+ ValidationError(
669
+ f"Missing required element: {required}",
670
+ path="$",
671
+ expected=required,
672
+ )
673
+ )
674
+
675
+ # Validate required attributes
676
+ for elem_name, attrs in self.required_attributes.items():
677
+ for elem in root.iter(elem_name):
678
+ for attr in attrs:
679
+ if attr not in elem.attrib:
680
+ errors.append(
681
+ ValidationError(
682
+ f"Missing required attribute: {attr}",
683
+ path=elem_name,
684
+ expected=attr,
685
+ )
686
+ )
687
+
688
+ # Validate element schemas
689
+ for elem_name, schema in self.element_schemas.items():
690
+ for elem in root.iter(elem_name):
691
+ self._validate_element(elem, schema, elem_name, errors)
692
+
693
+ return ValidationResult(
694
+ valid=len(errors) == 0,
695
+ errors=errors,
696
+ warnings=warnings,
697
+ schema_name=self.name,
698
+ checked_at=datetime.now().isoformat(),
699
+ )
700
+
701
+ def _validate_element(
702
+ self,
703
+ element: ElementTree.Element,
704
+ schema: Dict[str, Any],
705
+ path: str,
706
+ errors: List[ValidationError],
707
+ ) -> None:
708
+ """Validate element against schema."""
709
+ # Validate text content
710
+ if "text" in schema:
711
+ text_schema = schema["text"]
712
+ text = element.text or ""
713
+
714
+ if "pattern" in text_schema:
715
+ if not re.match(text_schema["pattern"], text):
716
+ errors.append(
717
+ ValidationError(
718
+ f"Element text does not match pattern",
719
+ path=path,
720
+ value=text,
721
+ expected=f"pattern: {text_schema['pattern']}",
722
+ )
723
+ )
724
+
725
+ if "minLength" in text_schema and len(text) < text_schema["minLength"]:
726
+ errors.append(
727
+ ValidationError(
728
+ f"Element text too short",
729
+ path=path,
730
+ value=text,
731
+ expected=f"minLength: {text_schema['minLength']}",
732
+ )
733
+ )
734
+
735
+ # Validate child count
736
+ if "minChildren" in schema:
737
+ if len(element) < schema["minChildren"]:
738
+ errors.append(
739
+ ValidationError(
740
+ f"Element has too few children",
741
+ path=path,
742
+ value=f"{len(element)} children",
743
+ expected=f"minChildren: {schema['minChildren']}",
744
+ )
745
+ )
746
+
747
+ if "maxChildren" in schema:
748
+ if len(element) > schema["maxChildren"]:
749
+ errors.append(
750
+ ValidationError(
751
+ f"Element has too many children",
752
+ path=path,
753
+ value=f"{len(element)} children",
754
+ expected=f"maxChildren: {schema['maxChildren']}",
755
+ )
756
+ )
757
+
758
+ def to_dict(self) -> Dict[str, Any]:
759
+ """Convert to dictionary representation."""
760
+ return {
761
+ "type": "xml_schema",
762
+ "name": self.name,
763
+ "root_element": self.root_element,
764
+ "required_elements": self.required_elements,
765
+ "required_attributes": self.required_attributes,
766
+ "element_schemas": self.element_schemas,
767
+ }
768
+
769
+
770
+ class CSVSchema(ReportSchema):
771
+ """CSV structure validator for reporter output.
772
+
773
+ Validates CSV output including headers, column count,
774
+ and column value constraints.
775
+
776
+ Example:
777
+ >>> schema = CSVSchema(
778
+ ... required_columns=["id", "name", "status"],
779
+ ... column_types={"id": "integer", "status": "enum:pass,fail"},
780
+ ... delimiter=",",
781
+ ... )
782
+ >>> result = schema.validate(csv_string)
783
+ """
784
+
785
+ def __init__(
786
+ self,
787
+ required_columns: Optional[List[str]] = None,
788
+ column_types: Optional[Dict[str, str]] = None,
789
+ delimiter: str = ",",
790
+ has_header: bool = True,
791
+ min_rows: Optional[int] = None,
792
+ max_rows: Optional[int] = None,
793
+ name: Optional[str] = None,
794
+ ) -> None:
795
+ """Initialize CSV Schema validator.
796
+
797
+ Args:
798
+ required_columns: List of required column names.
799
+ column_types: Dict mapping column names to type constraints.
800
+ delimiter: CSV delimiter character.
801
+ has_header: Whether CSV has a header row.
802
+ min_rows: Minimum number of data rows.
803
+ max_rows: Maximum number of data rows.
804
+ name: Optional schema name.
805
+ """
806
+ super().__init__(name)
807
+ self.required_columns = required_columns or []
808
+ self.column_types = column_types or {}
809
+ self.delimiter = delimiter
810
+ self.has_header = has_header
811
+ self.min_rows = min_rows
812
+ self.max_rows = max_rows
813
+
814
+ def validate(self, output: str) -> ValidationResult:
815
+ """Validate CSV output."""
816
+ import csv
817
+ from datetime import datetime
818
+ from io import StringIO
819
+
820
+ errors: List[ValidationError] = []
821
+ warnings: List[str] = []
822
+
823
+ if not isinstance(output, str):
824
+ errors.append(
825
+ ValidationError(
826
+ f"Expected string, got {type(output).__name__}",
827
+ path="$",
828
+ )
829
+ )
830
+ return ValidationResult(
831
+ valid=False,
832
+ errors=errors,
833
+ schema_name=self.name,
834
+ checked_at=datetime.now().isoformat(),
835
+ )
836
+
837
+ try:
838
+ reader = csv.reader(StringIO(output), delimiter=self.delimiter)
839
+ rows = list(reader)
840
+ except Exception as e:
841
+ errors.append(
842
+ ValidationError(
843
+ f"CSV parse error: {e}",
844
+ path="$",
845
+ )
846
+ )
847
+ return ValidationResult(
848
+ valid=False,
849
+ errors=errors,
850
+ schema_name=self.name,
851
+ checked_at=datetime.now().isoformat(),
852
+ )
853
+
854
+ if not rows:
855
+ if self.required_columns or self.min_rows:
856
+ errors.append(
857
+ ValidationError(
858
+ "CSV is empty",
859
+ path="$",
860
+ )
861
+ )
862
+ return ValidationResult(
863
+ valid=len(errors) == 0,
864
+ errors=errors,
865
+ schema_name=self.name,
866
+ checked_at=datetime.now().isoformat(),
867
+ )
868
+
869
+ # Extract header and data
870
+ if self.has_header:
871
+ headers = rows[0]
872
+ data_rows = rows[1:]
873
+ else:
874
+ headers = []
875
+ data_rows = rows
876
+
877
+ # Validate required columns
878
+ if self.has_header:
879
+ for col in self.required_columns:
880
+ if col not in headers:
881
+ errors.append(
882
+ ValidationError(
883
+ f"Missing required column: {col}",
884
+ path="header",
885
+ expected=col,
886
+ )
887
+ )
888
+
889
+ # Validate row count
890
+ if self.min_rows is not None and len(data_rows) < self.min_rows:
891
+ errors.append(
892
+ ValidationError(
893
+ f"Too few data rows",
894
+ path="$",
895
+ value=f"{len(data_rows)} rows",
896
+ expected=f"minRows: {self.min_rows}",
897
+ )
898
+ )
899
+
900
+ if self.max_rows is not None and len(data_rows) > self.max_rows:
901
+ errors.append(
902
+ ValidationError(
903
+ f"Too many data rows",
904
+ path="$",
905
+ value=f"{len(data_rows)} rows",
906
+ expected=f"maxRows: {self.max_rows}",
907
+ )
908
+ )
909
+
910
+ # Validate column types
911
+ if self.has_header and self.column_types:
912
+ for i, row in enumerate(data_rows):
913
+ for col_name, type_constraint in self.column_types.items():
914
+ if col_name in headers:
915
+ col_idx = headers.index(col_name)
916
+ if col_idx < len(row):
917
+ value = row[col_idx]
918
+ self._validate_column_type(
919
+ value, type_constraint, f"row[{i}].{col_name}", errors
920
+ )
921
+
922
+ return ValidationResult(
923
+ valid=len(errors) == 0,
924
+ errors=errors,
925
+ warnings=warnings,
926
+ schema_name=self.name,
927
+ checked_at=datetime.now().isoformat(),
928
+ )
929
+
930
+ def _validate_column_type(
931
+ self,
932
+ value: str,
933
+ type_constraint: str,
934
+ path: str,
935
+ errors: List[ValidationError],
936
+ ) -> None:
937
+ """Validate column value against type constraint."""
938
+ if type_constraint == "integer":
939
+ try:
940
+ int(value)
941
+ except ValueError:
942
+ errors.append(
943
+ ValidationError(
944
+ f"Value is not an integer",
945
+ path=path,
946
+ value=value,
947
+ expected="integer",
948
+ )
949
+ )
950
+
951
+ elif type_constraint == "number":
952
+ try:
953
+ float(value)
954
+ except ValueError:
955
+ errors.append(
956
+ ValidationError(
957
+ f"Value is not a number",
958
+ path=path,
959
+ value=value,
960
+ expected="number",
961
+ )
962
+ )
963
+
964
+ elif type_constraint == "boolean":
965
+ if value.lower() not in ("true", "false", "1", "0", "yes", "no"):
966
+ errors.append(
967
+ ValidationError(
968
+ f"Value is not a boolean",
969
+ path=path,
970
+ value=value,
971
+ expected="boolean",
972
+ )
973
+ )
974
+
975
+ elif type_constraint.startswith("enum:"):
976
+ allowed = type_constraint[5:].split(",")
977
+ if value not in allowed:
978
+ errors.append(
979
+ ValidationError(
980
+ f"Value not in enum",
981
+ path=path,
982
+ value=value,
983
+ expected=f"one of: {allowed}",
984
+ )
985
+ )
986
+
987
+ elif type_constraint.startswith("pattern:"):
988
+ pattern = type_constraint[8:]
989
+ if not re.match(pattern, value):
990
+ errors.append(
991
+ ValidationError(
992
+ f"Value does not match pattern",
993
+ path=path,
994
+ value=value,
995
+ expected=f"pattern: {pattern}",
996
+ )
997
+ )
998
+
999
+ def to_dict(self) -> Dict[str, Any]:
1000
+ """Convert to dictionary representation."""
1001
+ return {
1002
+ "type": "csv_schema",
1003
+ "name": self.name,
1004
+ "required_columns": self.required_columns,
1005
+ "column_types": self.column_types,
1006
+ "delimiter": self.delimiter,
1007
+ "has_header": self.has_header,
1008
+ "min_rows": self.min_rows,
1009
+ "max_rows": self.max_rows,
1010
+ }
1011
+
1012
+
1013
+ class TextSchema(ReportSchema):
1014
+ """Text/plain format validator for reporter output.
1015
+
1016
+ Validates text output using patterns, line constraints,
1017
+ and content requirements.
1018
+
1019
+ Example:
1020
+ >>> schema = TextSchema(
1021
+ ... required_patterns=[r"Summary:", r"Total: \d+"],
1022
+ ... forbidden_patterns=[r"ERROR:", r"FATAL:"],
1023
+ ... min_lines=5,
1024
+ ... )
1025
+ >>> result = schema.validate(text_output)
1026
+ """
1027
+
1028
+ def __init__(
1029
+ self,
1030
+ required_patterns: Optional[List[str]] = None,
1031
+ forbidden_patterns: Optional[List[str]] = None,
1032
+ min_lines: Optional[int] = None,
1033
+ max_lines: Optional[int] = None,
1034
+ min_length: Optional[int] = None,
1035
+ max_length: Optional[int] = None,
1036
+ encoding: str = "utf-8",
1037
+ name: Optional[str] = None,
1038
+ ) -> None:
1039
+ """Initialize Text Schema validator.
1040
+
1041
+ Args:
1042
+ required_patterns: Regex patterns that must appear in output.
1043
+ forbidden_patterns: Regex patterns that must not appear.
1044
+ min_lines: Minimum number of lines.
1045
+ max_lines: Maximum number of lines.
1046
+ min_length: Minimum total character length.
1047
+ max_length: Maximum total character length.
1048
+ encoding: Expected text encoding.
1049
+ name: Optional schema name.
1050
+ """
1051
+ super().__init__(name)
1052
+ self.required_patterns = required_patterns or []
1053
+ self.forbidden_patterns = forbidden_patterns or []
1054
+ self.min_lines = min_lines
1055
+ self.max_lines = max_lines
1056
+ self.min_length = min_length
1057
+ self.max_length = max_length
1058
+ self.encoding = encoding
1059
+
1060
+ def validate(self, output: Union[str, bytes]) -> ValidationResult:
1061
+ """Validate text output."""
1062
+ from datetime import datetime
1063
+
1064
+ errors: List[ValidationError] = []
1065
+ warnings: List[str] = []
1066
+
1067
+ # Convert bytes to string
1068
+ if isinstance(output, bytes):
1069
+ try:
1070
+ output = output.decode(self.encoding)
1071
+ except UnicodeDecodeError as e:
1072
+ errors.append(
1073
+ ValidationError(
1074
+ f"Encoding error: {e}",
1075
+ path="$",
1076
+ expected=f"encoding: {self.encoding}",
1077
+ )
1078
+ )
1079
+ return ValidationResult(
1080
+ valid=False,
1081
+ errors=errors,
1082
+ schema_name=self.name,
1083
+ checked_at=datetime.now().isoformat(),
1084
+ )
1085
+
1086
+ if not isinstance(output, str):
1087
+ errors.append(
1088
+ ValidationError(
1089
+ f"Expected string or bytes, got {type(output).__name__}",
1090
+ path="$",
1091
+ )
1092
+ )
1093
+ return ValidationResult(
1094
+ valid=False,
1095
+ errors=errors,
1096
+ schema_name=self.name,
1097
+ checked_at=datetime.now().isoformat(),
1098
+ )
1099
+
1100
+ # Validate length
1101
+ if self.min_length is not None and len(output) < self.min_length:
1102
+ errors.append(
1103
+ ValidationError(
1104
+ f"Text too short",
1105
+ path="$",
1106
+ value=f"{len(output)} chars",
1107
+ expected=f"minLength: {self.min_length}",
1108
+ )
1109
+ )
1110
+
1111
+ if self.max_length is not None and len(output) > self.max_length:
1112
+ errors.append(
1113
+ ValidationError(
1114
+ f"Text too long",
1115
+ path="$",
1116
+ value=f"{len(output)} chars",
1117
+ expected=f"maxLength: {self.max_length}",
1118
+ )
1119
+ )
1120
+
1121
+ # Validate line count
1122
+ lines = output.splitlines()
1123
+ if self.min_lines is not None and len(lines) < self.min_lines:
1124
+ errors.append(
1125
+ ValidationError(
1126
+ f"Too few lines",
1127
+ path="$",
1128
+ value=f"{len(lines)} lines",
1129
+ expected=f"minLines: {self.min_lines}",
1130
+ )
1131
+ )
1132
+
1133
+ if self.max_lines is not None and len(lines) > self.max_lines:
1134
+ errors.append(
1135
+ ValidationError(
1136
+ f"Too many lines",
1137
+ path="$",
1138
+ value=f"{len(lines)} lines",
1139
+ expected=f"maxLines: {self.max_lines}",
1140
+ )
1141
+ )
1142
+
1143
+ # Validate required patterns
1144
+ for pattern in self.required_patterns:
1145
+ if not re.search(pattern, output):
1146
+ errors.append(
1147
+ ValidationError(
1148
+ f"Required pattern not found",
1149
+ path="$",
1150
+ expected=f"pattern: {pattern}",
1151
+ )
1152
+ )
1153
+
1154
+ # Validate forbidden patterns
1155
+ for pattern in self.forbidden_patterns:
1156
+ match = re.search(pattern, output)
1157
+ if match:
1158
+ errors.append(
1159
+ ValidationError(
1160
+ f"Forbidden pattern found",
1161
+ path="$",
1162
+ value=match.group(),
1163
+ expected=f"not: {pattern}",
1164
+ )
1165
+ )
1166
+
1167
+ return ValidationResult(
1168
+ valid=len(errors) == 0,
1169
+ errors=errors,
1170
+ warnings=warnings,
1171
+ schema_name=self.name,
1172
+ checked_at=datetime.now().isoformat(),
1173
+ )
1174
+
1175
+ def to_dict(self) -> Dict[str, Any]:
1176
+ """Convert to dictionary representation."""
1177
+ return {
1178
+ "type": "text_schema",
1179
+ "name": self.name,
1180
+ "required_patterns": self.required_patterns,
1181
+ "forbidden_patterns": self.forbidden_patterns,
1182
+ "min_lines": self.min_lines,
1183
+ "max_lines": self.max_lines,
1184
+ "min_length": self.min_length,
1185
+ "max_length": self.max_length,
1186
+ "encoding": self.encoding,
1187
+ }
1188
+
1189
+
1190
+ # Schema registry
1191
+ _schema_registry: Dict[str, ReportSchema] = {}
1192
+
1193
+
1194
+ def register_schema(name: str, schema: ReportSchema) -> None:
1195
+ """Register a schema for a reporter.
1196
+
1197
+ Args:
1198
+ name: Schema name (typically reporter name).
1199
+ schema: The schema to register.
1200
+
1201
+ Example:
1202
+ >>> schema = JSONSchema({...})
1203
+ >>> register_schema("my_reporter", schema)
1204
+ """
1205
+ _schema_registry[name] = schema
1206
+
1207
+
1208
+ def get_schema(name: str) -> Optional[ReportSchema]:
1209
+ """Get a registered schema by name.
1210
+
1211
+ Args:
1212
+ name: Schema name to look up.
1213
+
1214
+ Returns:
1215
+ The registered schema, or None if not found.
1216
+ """
1217
+ return _schema_registry.get(name)
1218
+
1219
+
1220
+ def unregister_schema(name: str) -> bool:
1221
+ """Unregister a schema.
1222
+
1223
+ Args:
1224
+ name: Schema name to unregister.
1225
+
1226
+ Returns:
1227
+ True if schema was removed, False if not found.
1228
+ """
1229
+ if name in _schema_registry:
1230
+ del _schema_registry[name]
1231
+ return True
1232
+ return False
1233
+
1234
+
1235
+ def validate_output(
1236
+ output: Any,
1237
+ schema: Optional[ReportSchema] = None,
1238
+ schema_name: Optional[str] = None,
1239
+ ) -> ValidationResult:
1240
+ """Validate output against a schema.
1241
+
1242
+ Args:
1243
+ output: The output to validate.
1244
+ schema: Schema to validate against.
1245
+ schema_name: Name of a registered schema to use.
1246
+
1247
+ Returns:
1248
+ ValidationResult with validation status.
1249
+
1250
+ Raises:
1251
+ ValueError: If neither schema nor schema_name provided.
1252
+ """
1253
+ if schema is None and schema_name is not None:
1254
+ schema = get_schema(schema_name)
1255
+ if schema is None:
1256
+ raise ValueError(f"No schema registered with name: {schema_name}")
1257
+
1258
+ if schema is None:
1259
+ raise ValueError("Must provide either schema or schema_name")
1260
+
1261
+ return schema.validate(output)
1262
+
1263
+
1264
+ def validate_reporter_output(
1265
+ schema: Optional[ReportSchema] = None,
1266
+ schema_name: Optional[str] = None,
1267
+ raise_on_error: bool = False,
1268
+ ) -> Callable:
1269
+ """Decorator to validate reporter output.
1270
+
1271
+ Args:
1272
+ schema: Schema to validate against.
1273
+ schema_name: Name of registered schema to use.
1274
+ raise_on_error: If True, raise SchemaError on validation failure.
1275
+
1276
+ Returns:
1277
+ Decorator function.
1278
+
1279
+ Example:
1280
+ >>> @validate_reporter_output(schema=my_schema, raise_on_error=True)
1281
+ ... def render(self, results):
1282
+ ... return {"summary": ..., "results": ...}
1283
+ """
1284
+
1285
+ def decorator(func: Callable) -> Callable:
1286
+ def wrapper(*args, **kwargs):
1287
+ output = func(*args, **kwargs)
1288
+
1289
+ result = validate_output(output, schema=schema, schema_name=schema_name)
1290
+
1291
+ if raise_on_error:
1292
+ result.raise_if_invalid()
1293
+
1294
+ return output
1295
+
1296
+ return wrapper
1297
+
1298
+ return decorator
1299
+
1300
+
1301
+ def infer_schema(output: Any) -> ReportSchema:
1302
+ """Infer a schema from sample output.
1303
+
1304
+ This function attempts to generate a schema based on
1305
+ the structure of the provided output.
1306
+
1307
+ Args:
1308
+ output: Sample output to infer schema from.
1309
+
1310
+ Returns:
1311
+ Inferred ReportSchema.
1312
+
1313
+ Example:
1314
+ >>> sample = {"name": "test", "count": 5, "items": [1, 2, 3]}
1315
+ >>> schema = infer_schema(sample)
1316
+ >>> result = schema.validate(new_output)
1317
+ """
1318
+ if isinstance(output, dict):
1319
+ return _infer_json_schema(output)
1320
+ elif isinstance(output, str):
1321
+ if output.strip().startswith("<"):
1322
+ return _infer_xml_schema(output)
1323
+ elif "," in output.split("\n")[0]:
1324
+ return _infer_csv_schema(output)
1325
+ else:
1326
+ return _infer_text_schema(output)
1327
+ else:
1328
+ # Default to JSON schema for other types
1329
+ return JSONSchema({"type": _python_to_json_type(type(output))})
1330
+
1331
+
1332
+ def _python_to_json_type(python_type: type) -> str:
1333
+ """Convert Python type to JSON Schema type."""
1334
+ type_map = {
1335
+ str: "string",
1336
+ int: "integer",
1337
+ float: "number",
1338
+ bool: "boolean",
1339
+ list: "array",
1340
+ dict: "object",
1341
+ type(None): "null",
1342
+ }
1343
+ return type_map.get(python_type, "string")
1344
+
1345
+
1346
+ def _infer_json_schema(obj: Dict[str, Any]) -> JSONSchema:
1347
+ """Infer JSON Schema from dictionary."""
1348
+
1349
+ def infer_property_schema(value: Any) -> Dict[str, Any]:
1350
+ if value is None:
1351
+ return {"type": "null"}
1352
+ elif isinstance(value, bool):
1353
+ return {"type": "boolean"}
1354
+ elif isinstance(value, int):
1355
+ return {"type": "integer"}
1356
+ elif isinstance(value, float):
1357
+ return {"type": "number"}
1358
+ elif isinstance(value, str):
1359
+ return {"type": "string"}
1360
+ elif isinstance(value, list):
1361
+ if value:
1362
+ item_schema = infer_property_schema(value[0])
1363
+ return {"type": "array", "items": item_schema}
1364
+ return {"type": "array"}
1365
+ elif isinstance(value, dict):
1366
+ properties = {}
1367
+ for k, v in value.items():
1368
+ properties[k] = infer_property_schema(v)
1369
+ return {"type": "object", "properties": properties}
1370
+ return {}
1371
+
1372
+ properties = {}
1373
+ required = []
1374
+
1375
+ for key, value in obj.items():
1376
+ properties[key] = infer_property_schema(value)
1377
+ if value is not None:
1378
+ required.append(key)
1379
+
1380
+ return JSONSchema(
1381
+ {
1382
+ "type": "object",
1383
+ "properties": properties,
1384
+ "required": required,
1385
+ },
1386
+ name="inferred",
1387
+ )
1388
+
1389
+
1390
+ def _infer_xml_schema(xml_str: str) -> XMLSchema:
1391
+ """Infer XML Schema from XML string."""
1392
+ try:
1393
+ root = ElementTree.fromstring(xml_str)
1394
+ elements = {elem.tag for elem in root.iter()}
1395
+ elements.discard(root.tag)
1396
+
1397
+ return XMLSchema(
1398
+ root_element=root.tag,
1399
+ required_elements=list(elements),
1400
+ name="inferred",
1401
+ )
1402
+ except Exception:
1403
+ return XMLSchema(root_element="root", name="inferred")
1404
+
1405
+
1406
+ def _infer_csv_schema(csv_str: str) -> CSVSchema:
1407
+ """Infer CSV Schema from CSV string."""
1408
+ import csv
1409
+ from io import StringIO
1410
+
1411
+ try:
1412
+ reader = csv.reader(StringIO(csv_str))
1413
+ rows = list(reader)
1414
+
1415
+ if rows:
1416
+ headers = rows[0]
1417
+ return CSVSchema(
1418
+ required_columns=headers,
1419
+ has_header=True,
1420
+ min_rows=max(0, len(rows) - 1),
1421
+ name="inferred",
1422
+ )
1423
+ except Exception:
1424
+ pass
1425
+
1426
+ return CSVSchema(name="inferred")
1427
+
1428
+
1429
+ def _infer_text_schema(text: str) -> TextSchema:
1430
+ """Infer Text Schema from text string."""
1431
+ lines = text.splitlines()
1432
+
1433
+ return TextSchema(
1434
+ min_lines=len(lines),
1435
+ min_length=len(text) // 2, # Allow some variation
1436
+ max_length=len(text) * 2,
1437
+ name="inferred",
1438
+ )
1439
+
1440
+
1441
+ def merge_schemas(
1442
+ schemas: List[ReportSchema],
1443
+ name: Optional[str] = None,
1444
+ ) -> ReportSchema:
1445
+ """Merge multiple schemas into a composite schema.
1446
+
1447
+ The merged schema validates against all provided schemas.
1448
+
1449
+ Args:
1450
+ schemas: List of schemas to merge.
1451
+ name: Optional name for the merged schema.
1452
+
1453
+ Returns:
1454
+ A new schema that combines all validations.
1455
+
1456
+ Example:
1457
+ >>> schema1 = JSONSchema({"type": "object"})
1458
+ >>> schema2 = TextSchema(min_length=10)
1459
+ >>> merged = merge_schemas([schema1, schema2])
1460
+ """
1461
+
1462
+ class CompositeSchema(ReportSchema):
1463
+ def __init__(self, schemas: List[ReportSchema], name: Optional[str]) -> None:
1464
+ super().__init__(name or "composite")
1465
+ self.schemas = schemas
1466
+
1467
+ def validate(self, output: Any) -> ValidationResult:
1468
+ from datetime import datetime
1469
+
1470
+ all_errors: List[ValidationError] = []
1471
+ all_warnings: List[str] = []
1472
+
1473
+ for schema in self.schemas:
1474
+ result = schema.validate(output)
1475
+ all_errors.extend(result.errors)
1476
+ all_warnings.extend(result.warnings)
1477
+
1478
+ return ValidationResult(
1479
+ valid=len(all_errors) == 0,
1480
+ errors=all_errors,
1481
+ warnings=all_warnings,
1482
+ schema_name=self.name,
1483
+ checked_at=datetime.now().isoformat(),
1484
+ )
1485
+
1486
+ def to_dict(self) -> Dict[str, Any]:
1487
+ return {
1488
+ "type": "composite",
1489
+ "name": self.name,
1490
+ "schemas": [s.to_dict() for s in self.schemas],
1491
+ }
1492
+
1493
+ return CompositeSchema(schemas, name)