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,954 @@
1
+ """Policy engine and evaluators for RBAC.
2
+
3
+ This module provides the policy evaluation engine and various policy
4
+ evaluators for making access control decisions.
5
+
6
+ Architecture:
7
+ The policy engine follows a pipeline design:
8
+
9
+ AccessContext
10
+
11
+ ├── PolicyEngine
12
+ │ │
13
+ │ ├── PolicyEvaluator 1 (e.g., RoleBasedEvaluator)
14
+ │ │
15
+ │ ├── PolicyEvaluator 2 (e.g., ABACEvaluator)
16
+ │ │
17
+ │ └── PolicyEvaluator N
18
+
19
+ v
20
+ AccessDecision
21
+
22
+ Policy Combination:
23
+ - DENY_OVERRIDES: Any deny results in deny (default, most secure)
24
+ - ALLOW_OVERRIDES: Any allow results in allow
25
+ - FIRST_APPLICABLE: First matching policy decides
26
+ - UNANIMOUS: All must agree
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import threading
32
+ import time
33
+ from abc import ABC
34
+ from dataclasses import dataclass, field
35
+ from datetime import datetime, timezone
36
+ from enum import Enum
37
+ from typing import Any, Callable, Sequence
38
+
39
+ from truthound.rbac.core import (
40
+ AccessContext,
41
+ AccessDecision,
42
+ Condition,
43
+ Permission,
44
+ PermissionEffect,
45
+ PolicyEvaluator,
46
+ Principal,
47
+ Role,
48
+ RoleStore,
49
+ PrincipalStore,
50
+ PermissionDeniedError,
51
+ PolicyEvaluationError,
52
+ )
53
+
54
+
55
+ # =============================================================================
56
+ # Policy Combination Algorithms
57
+ # =============================================================================
58
+
59
+
60
+ class PolicyCombination(Enum):
61
+ """Policy combination algorithms."""
62
+
63
+ DENY_OVERRIDES = "deny_overrides" # Any deny wins
64
+ ALLOW_OVERRIDES = "allow_overrides" # Any allow wins
65
+ FIRST_APPLICABLE = "first_applicable" # First match wins
66
+ UNANIMOUS = "unanimous" # All must allow
67
+
68
+
69
+ # =============================================================================
70
+ # Policy Types
71
+ # =============================================================================
72
+
73
+
74
+ @dataclass
75
+ class Policy:
76
+ """A policy that defines access rules.
77
+
78
+ Policies bind permissions to subjects (principals, roles) with
79
+ optional conditions.
80
+
81
+ Example:
82
+ >>> policy = Policy(
83
+ ... id="data_analyst_read",
84
+ ... name="Data Analysts Read Access",
85
+ ... effect=PermissionEffect.ALLOW,
86
+ ... subjects=["role:data_analyst"],
87
+ ... resources=["dataset:*"],
88
+ ... actions=["read", "list"],
89
+ ... )
90
+ """
91
+
92
+ id: str
93
+ name: str
94
+ description: str = ""
95
+
96
+ # Effect when policy matches
97
+ effect: PermissionEffect = PermissionEffect.ALLOW
98
+
99
+ # Who this policy applies to
100
+ subjects: list[str] = field(default_factory=list) # e.g., ["role:admin", "user:123"]
101
+
102
+ # What resources this policy covers
103
+ resources: list[str] = field(default_factory=list) # e.g., ["dataset:*", "validation:*"]
104
+
105
+ # What actions are allowed/denied
106
+ actions: list[str] = field(default_factory=list) # e.g., ["read", "write", "*"]
107
+
108
+ # Conditions for conditional policies (ABAC)
109
+ conditions: list[Condition] = field(default_factory=list)
110
+
111
+ # Priority (higher = evaluated first)
112
+ priority: int = 0
113
+
114
+ # Status
115
+ enabled: bool = True
116
+
117
+ # Tenant scope
118
+ tenant_id: str | None = None
119
+
120
+ # Metadata
121
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
122
+ metadata: dict[str, Any] = field(default_factory=dict)
123
+
124
+ def matches_subject(self, principal: Principal) -> bool:
125
+ """Check if the policy matches the principal."""
126
+ if not self.subjects:
127
+ return True # Empty subjects means applies to all
128
+
129
+ for subject in self.subjects:
130
+ if subject == "*":
131
+ return True
132
+
133
+ if ":" in subject:
134
+ subject_type, subject_id = subject.split(":", 1)
135
+
136
+ if subject_type == "role":
137
+ if subject_id == "*" or subject_id in principal.roles:
138
+ return True
139
+ elif subject_type == "user":
140
+ if subject_id == "*" or subject_id == principal.id:
141
+ return True
142
+ elif subject_type == "type":
143
+ if subject_id == principal.type.value:
144
+ return True
145
+ else:
146
+ # Plain role name
147
+ if subject in principal.roles:
148
+ return True
149
+
150
+ return False
151
+
152
+ def matches_resource(self, resource: str) -> bool:
153
+ """Check if the policy matches the resource."""
154
+ if not self.resources:
155
+ return True # Empty resources means applies to all
156
+
157
+ for pattern in self.resources:
158
+ if pattern == "*":
159
+ return True
160
+
161
+ if self._matches_pattern(pattern, resource):
162
+ return True
163
+
164
+ return False
165
+
166
+ def matches_action(self, action: str) -> bool:
167
+ """Check if the policy matches the action."""
168
+ if not self.actions:
169
+ return True # Empty actions means applies to all
170
+
171
+ for allowed_action in self.actions:
172
+ if allowed_action == "*" or allowed_action == action:
173
+ return True
174
+
175
+ return False
176
+
177
+ def _matches_pattern(self, pattern: str, value: str) -> bool:
178
+ """Check if a pattern matches a value (supports wildcards)."""
179
+ if pattern == "*":
180
+ return True
181
+ if "*" not in pattern:
182
+ return pattern == value
183
+
184
+ # Simple wildcard matching
185
+ if pattern.endswith("*"):
186
+ return value.startswith(pattern[:-1])
187
+ if pattern.startswith("*"):
188
+ return value.endswith(pattern[1:])
189
+
190
+ # Contains wildcard
191
+ parts = pattern.split("*")
192
+ if len(parts) == 2:
193
+ return value.startswith(parts[0]) and value.endswith(parts[1])
194
+
195
+ return pattern == value
196
+
197
+ def evaluate_conditions(self, context: AccessContext) -> bool:
198
+ """Evaluate all conditions for this policy."""
199
+ if not self.conditions:
200
+ return True
201
+
202
+ for condition in self.conditions:
203
+ if not condition.evaluate(context):
204
+ return False
205
+
206
+ return True
207
+
208
+ def to_dict(self) -> dict[str, Any]:
209
+ """Convert to dictionary."""
210
+ return {
211
+ "id": self.id,
212
+ "name": self.name,
213
+ "description": self.description,
214
+ "effect": self.effect.value,
215
+ "subjects": self.subjects,
216
+ "resources": self.resources,
217
+ "actions": self.actions,
218
+ "conditions": [c.to_dict() for c in self.conditions],
219
+ "priority": self.priority,
220
+ "enabled": self.enabled,
221
+ "tenant_id": self.tenant_id,
222
+ "created_at": self.created_at.isoformat(),
223
+ "metadata": self.metadata,
224
+ }
225
+
226
+
227
+ # =============================================================================
228
+ # Policy Evaluators
229
+ # =============================================================================
230
+
231
+
232
+ class RoleBasedEvaluator(PolicyEvaluator):
233
+ """Role-based access control evaluator.
234
+
235
+ Checks permissions based on roles assigned to the principal.
236
+
237
+ Example:
238
+ >>> evaluator = RoleBasedEvaluator(role_store)
239
+ >>> decision = evaluator.evaluate(context)
240
+ """
241
+
242
+ def __init__(
243
+ self,
244
+ role_store: RoleStore,
245
+ deny_by_default: bool = True,
246
+ ) -> None:
247
+ self._role_store = role_store
248
+ self._deny_by_default = deny_by_default
249
+
250
+ @property
251
+ def name(self) -> str:
252
+ return "role_based"
253
+
254
+ @property
255
+ def priority(self) -> int:
256
+ return 100
257
+
258
+ def evaluate(self, context: AccessContext) -> AccessDecision:
259
+ """Evaluate access based on roles."""
260
+ start_time = time.time()
261
+
262
+ if context.principal is None:
263
+ return AccessDecision.deny("No principal in context")
264
+
265
+ principal = context.principal
266
+ required_permission = context.get_required_permission()
267
+
268
+ # Check direct permissions first
269
+ for perm in principal.direct_permissions:
270
+ if perm.matches(required_permission):
271
+ if perm.effect == PermissionEffect.DENY:
272
+ return AccessDecision.deny(
273
+ f"Permission explicitly denied: {perm}"
274
+ )
275
+ return AccessDecision.allow(
276
+ f"Direct permission granted: {perm}",
277
+ [perm],
278
+ )
279
+
280
+ # Check role permissions
281
+ matching_permissions: list[Permission] = []
282
+ for role_id in principal.roles:
283
+ permissions = self._role_store.get_all_permissions(role_id)
284
+ for perm in permissions:
285
+ if perm.matches(required_permission):
286
+ if perm.effect == PermissionEffect.DENY:
287
+ return AccessDecision.deny(
288
+ f"Permission denied by role {role_id}: {perm}"
289
+ )
290
+ matching_permissions.append(perm)
291
+
292
+ if matching_permissions:
293
+ elapsed = (time.time() - start_time) * 1000
294
+ decision = AccessDecision.allow(
295
+ f"Permission granted by roles",
296
+ matching_permissions,
297
+ )
298
+ decision.evaluation_time_ms = elapsed
299
+ return decision
300
+
301
+ if self._deny_by_default:
302
+ return AccessDecision.deny(
303
+ f"No matching permission for {required_permission}"
304
+ )
305
+
306
+ # Return neutral (let other evaluators decide)
307
+ return AccessDecision(
308
+ allowed=False,
309
+ reason="No matching role permission",
310
+ )
311
+
312
+
313
+ class PolicyBasedEvaluator(PolicyEvaluator):
314
+ """Policy-based access control evaluator.
315
+
316
+ Evaluates access based on explicit policies.
317
+
318
+ Example:
319
+ >>> evaluator = PolicyBasedEvaluator()
320
+ >>> evaluator.add_policy(Policy(...))
321
+ >>> decision = evaluator.evaluate(context)
322
+ """
323
+
324
+ def __init__(
325
+ self,
326
+ combination: PolicyCombination = PolicyCombination.DENY_OVERRIDES,
327
+ ) -> None:
328
+ self._policies: list[Policy] = []
329
+ self._combination = combination
330
+ self._lock = threading.RLock()
331
+
332
+ @property
333
+ def name(self) -> str:
334
+ return "policy_based"
335
+
336
+ @property
337
+ def priority(self) -> int:
338
+ return 90
339
+
340
+ def add_policy(self, policy: Policy) -> None:
341
+ """Add a policy."""
342
+ with self._lock:
343
+ self._policies.append(policy)
344
+ # Sort by priority (descending)
345
+ self._policies.sort(key=lambda p: p.priority, reverse=True)
346
+
347
+ def remove_policy(self, policy_id: str) -> bool:
348
+ """Remove a policy by ID."""
349
+ with self._lock:
350
+ for i, policy in enumerate(self._policies):
351
+ if policy.id == policy_id:
352
+ del self._policies[i]
353
+ return True
354
+ return False
355
+
356
+ def get_policy(self, policy_id: str) -> Policy | None:
357
+ """Get a policy by ID."""
358
+ with self._lock:
359
+ for policy in self._policies:
360
+ if policy.id == policy_id:
361
+ return policy
362
+ return None
363
+
364
+ def evaluate(self, context: AccessContext) -> AccessDecision:
365
+ """Evaluate access based on policies."""
366
+ start_time = time.time()
367
+
368
+ if context.principal is None:
369
+ return AccessDecision.deny("No principal in context")
370
+
371
+ resource = context.resource
372
+ action = context.action if isinstance(context.action, str) else context.action.value
373
+
374
+ matching_policies: list[tuple[Policy, PermissionEffect]] = []
375
+
376
+ with self._lock:
377
+ for policy in self._policies:
378
+ if not policy.enabled:
379
+ continue
380
+
381
+ # Check tenant scope
382
+ if policy.tenant_id and policy.tenant_id != context.tenant_id:
383
+ continue
384
+
385
+ # Check if policy matches
386
+ if not policy.matches_subject(context.principal):
387
+ continue
388
+ if not policy.matches_resource(resource):
389
+ continue
390
+ if not policy.matches_action(action):
391
+ continue
392
+ if not policy.evaluate_conditions(context):
393
+ continue
394
+
395
+ matching_policies.append((policy, policy.effect))
396
+
397
+ if not matching_policies:
398
+ return AccessDecision(
399
+ allowed=False,
400
+ reason="No matching policy",
401
+ )
402
+
403
+ # Apply combination algorithm
404
+ decision = self._combine_decisions(matching_policies)
405
+ decision.evaluation_time_ms = (time.time() - start_time) * 1000
406
+ return decision
407
+
408
+ def _combine_decisions(
409
+ self,
410
+ matches: list[tuple[Policy, PermissionEffect]],
411
+ ) -> AccessDecision:
412
+ """Combine policy decisions based on combination algorithm."""
413
+ if self._combination == PolicyCombination.DENY_OVERRIDES:
414
+ # Any deny wins
415
+ for policy, effect in matches:
416
+ if effect == PermissionEffect.DENY:
417
+ return AccessDecision.deny(
418
+ f"Denied by policy: {policy.name}"
419
+ )
420
+ # All allows
421
+ policy, _ = matches[0]
422
+ return AccessDecision.allow(
423
+ f"Allowed by policy: {policy.name}",
424
+ )
425
+
426
+ elif self._combination == PolicyCombination.ALLOW_OVERRIDES:
427
+ # Any allow wins
428
+ for policy, effect in matches:
429
+ if effect == PermissionEffect.ALLOW:
430
+ return AccessDecision.allow(
431
+ f"Allowed by policy: {policy.name}",
432
+ )
433
+ # All denies
434
+ policy, _ = matches[0]
435
+ return AccessDecision.deny(
436
+ f"Denied by policy: {policy.name}"
437
+ )
438
+
439
+ elif self._combination == PolicyCombination.FIRST_APPLICABLE:
440
+ # First match wins
441
+ policy, effect = matches[0]
442
+ if effect == PermissionEffect.ALLOW:
443
+ return AccessDecision.allow(
444
+ f"Allowed by policy: {policy.name}",
445
+ )
446
+ return AccessDecision.deny(
447
+ f"Denied by policy: {policy.name}"
448
+ )
449
+
450
+ elif self._combination == PolicyCombination.UNANIMOUS:
451
+ # All must allow
452
+ for policy, effect in matches:
453
+ if effect == PermissionEffect.DENY:
454
+ return AccessDecision.deny(
455
+ f"Denied by policy: {policy.name} (unanimous required)"
456
+ )
457
+ policy, _ = matches[0]
458
+ return AccessDecision.allow(
459
+ f"Unanimously allowed",
460
+ )
461
+
462
+ return AccessDecision.deny("Unknown combination algorithm")
463
+
464
+
465
+ class ABACEvaluator(PolicyEvaluator):
466
+ """Attribute-Based Access Control evaluator.
467
+
468
+ Evaluates access based on attributes of the principal, resource,
469
+ and environment.
470
+
471
+ Example:
472
+ >>> evaluator = ABACEvaluator()
473
+ >>> evaluator.add_rule(
474
+ ... condition=Condition("resource.owner_id", ConditionOperator.EQUALS, "${principal.id}"),
475
+ ... effect=PermissionEffect.ALLOW,
476
+ ... )
477
+ """
478
+
479
+ def __init__(self) -> None:
480
+ self._rules: list[tuple[list[Condition], PermissionEffect, str]] = []
481
+ self._lock = threading.RLock()
482
+
483
+ @property
484
+ def name(self) -> str:
485
+ return "abac"
486
+
487
+ @property
488
+ def priority(self) -> int:
489
+ return 80
490
+
491
+ def add_rule(
492
+ self,
493
+ conditions: list[Condition],
494
+ effect: PermissionEffect = PermissionEffect.ALLOW,
495
+ description: str = "",
496
+ ) -> None:
497
+ """Add an ABAC rule."""
498
+ with self._lock:
499
+ self._rules.append((conditions, effect, description))
500
+
501
+ def evaluate(self, context: AccessContext) -> AccessDecision:
502
+ """Evaluate access based on attributes."""
503
+ start_time = time.time()
504
+
505
+ if context.principal is None:
506
+ return AccessDecision.deny("No principal in context")
507
+
508
+ with self._lock:
509
+ for conditions, effect, description in self._rules:
510
+ all_match = True
511
+ for condition in conditions:
512
+ if not condition.evaluate(context):
513
+ all_match = False
514
+ break
515
+
516
+ if all_match:
517
+ elapsed = (time.time() - start_time) * 1000
518
+ if effect == PermissionEffect.ALLOW:
519
+ decision = AccessDecision.allow(
520
+ f"ABAC rule matched: {description}"
521
+ )
522
+ else:
523
+ decision = AccessDecision.deny(
524
+ f"ABAC rule denied: {description}"
525
+ )
526
+ decision.evaluation_time_ms = elapsed
527
+ return decision
528
+
529
+ return AccessDecision(
530
+ allowed=False,
531
+ reason="No matching ABAC rule",
532
+ )
533
+
534
+
535
+ class OwnershipEvaluator(PolicyEvaluator):
536
+ """Ownership-based access control evaluator.
537
+
538
+ Grants access if the principal owns the resource.
539
+
540
+ Example:
541
+ >>> evaluator = OwnershipEvaluator(owner_field="owner_id")
542
+ >>> # Principal can access resources they own
543
+ """
544
+
545
+ def __init__(
546
+ self,
547
+ owner_field: str = "owner_id",
548
+ actions: list[str] | None = None, # Actions allowed for owners
549
+ ) -> None:
550
+ self._owner_field = owner_field
551
+ self._actions = actions or ["read", "update", "delete"]
552
+
553
+ @property
554
+ def name(self) -> str:
555
+ return "ownership"
556
+
557
+ @property
558
+ def priority(self) -> int:
559
+ return 70
560
+
561
+ def evaluate(self, context: AccessContext) -> AccessDecision:
562
+ """Evaluate access based on ownership."""
563
+ if context.principal is None:
564
+ return AccessDecision.deny("No principal in context")
565
+
566
+ action = context.action if isinstance(context.action, str) else context.action.value
567
+
568
+ # Check if action is allowed for owners
569
+ if action not in self._actions and "*" not in self._actions:
570
+ return AccessDecision(
571
+ allowed=False,
572
+ reason="Action not covered by ownership evaluator",
573
+ )
574
+
575
+ # Get owner from resource attributes
576
+ owner_id = context.resource_attributes.get(self._owner_field)
577
+ if owner_id is None:
578
+ return AccessDecision(
579
+ allowed=False,
580
+ reason="No owner information in resource",
581
+ )
582
+
583
+ # Check ownership
584
+ if owner_id == context.principal.id:
585
+ return AccessDecision.allow(
586
+ f"Principal owns the resource"
587
+ )
588
+
589
+ return AccessDecision(
590
+ allowed=False,
591
+ reason="Principal does not own the resource",
592
+ )
593
+
594
+
595
+ class TenantIsolationEvaluator(PolicyEvaluator):
596
+ """Tenant isolation evaluator.
597
+
598
+ Ensures principals can only access resources within their tenant.
599
+
600
+ Example:
601
+ >>> evaluator = TenantIsolationEvaluator()
602
+ >>> # Denies cross-tenant access
603
+ """
604
+
605
+ def __init__(
606
+ self,
607
+ tenant_field: str = "tenant_id",
608
+ enforce_strict: bool = True,
609
+ ) -> None:
610
+ self._tenant_field = tenant_field
611
+ self._enforce_strict = enforce_strict
612
+
613
+ @property
614
+ def name(self) -> str:
615
+ return "tenant_isolation"
616
+
617
+ @property
618
+ def priority(self) -> int:
619
+ return 200 # High priority - check early
620
+
621
+ def evaluate(self, context: AccessContext) -> AccessDecision:
622
+ """Evaluate tenant isolation."""
623
+ if context.principal is None:
624
+ return AccessDecision.deny("No principal in context")
625
+
626
+ principal_tenant = context.principal.tenant_id
627
+ resource_tenant = context.resource_attributes.get(self._tenant_field)
628
+
629
+ # If no tenant info, depends on strict mode
630
+ if resource_tenant is None:
631
+ if self._enforce_strict:
632
+ return AccessDecision(
633
+ allowed=False,
634
+ reason="No tenant information in resource",
635
+ )
636
+ return AccessDecision(
637
+ allowed=True,
638
+ reason="Tenant check skipped - no tenant info",
639
+ )
640
+
641
+ # Check tenant match
642
+ if principal_tenant != resource_tenant:
643
+ return AccessDecision.deny(
644
+ f"Cross-tenant access denied: {principal_tenant} != {resource_tenant}"
645
+ )
646
+
647
+ return AccessDecision.allow("Same tenant access")
648
+
649
+
650
+ class SuperuserEvaluator(PolicyEvaluator):
651
+ """Superuser evaluator.
652
+
653
+ Grants all permissions to superuser/admin principals.
654
+
655
+ Example:
656
+ >>> evaluator = SuperuserEvaluator(superuser_roles={"superadmin"})
657
+ """
658
+
659
+ def __init__(
660
+ self,
661
+ superuser_roles: set[str] | None = None,
662
+ superuser_ids: set[str] | None = None,
663
+ ) -> None:
664
+ self._superuser_roles = superuser_roles or {"superadmin", "system_admin"}
665
+ self._superuser_ids = superuser_ids or {"system"}
666
+
667
+ @property
668
+ def name(self) -> str:
669
+ return "superuser"
670
+
671
+ @property
672
+ def priority(self) -> int:
673
+ return 1000 # Highest priority
674
+
675
+ def evaluate(self, context: AccessContext) -> AccessDecision:
676
+ """Check if principal is a superuser."""
677
+ if context.principal is None:
678
+ return AccessDecision(allowed=False, reason="No principal")
679
+
680
+ # Check superuser IDs
681
+ if context.principal.id in self._superuser_ids:
682
+ return AccessDecision.allow("Superuser access (by ID)")
683
+
684
+ # Check superuser roles
685
+ if context.principal.roles & self._superuser_roles:
686
+ return AccessDecision.allow("Superuser access (by role)")
687
+
688
+ return AccessDecision(
689
+ allowed=False,
690
+ reason="Not a superuser",
691
+ )
692
+
693
+
694
+ # =============================================================================
695
+ # Policy Engine
696
+ # =============================================================================
697
+
698
+
699
+ @dataclass
700
+ class PolicyEngineConfig:
701
+ """Configuration for the policy engine."""
702
+
703
+ combination: PolicyCombination = PolicyCombination.DENY_OVERRIDES
704
+ deny_by_default: bool = True
705
+ log_decisions: bool = True
706
+ cache_decisions: bool = True
707
+ cache_ttl_seconds: int = 60
708
+
709
+
710
+ class PolicyEngine:
711
+ """Central policy engine for access control.
712
+
713
+ Coordinates multiple policy evaluators to make access decisions.
714
+
715
+ Example:
716
+ >>> engine = PolicyEngine()
717
+ >>> engine.add_evaluator(RoleBasedEvaluator(role_store))
718
+ >>> engine.add_evaluator(OwnershipEvaluator())
719
+ >>>
720
+ >>> decision = engine.evaluate(context)
721
+ >>> if decision.allowed:
722
+ ... process_request()
723
+ """
724
+
725
+ def __init__(
726
+ self,
727
+ config: PolicyEngineConfig | None = None,
728
+ ) -> None:
729
+ self._config = config or PolicyEngineConfig()
730
+ self._evaluators: list[PolicyEvaluator] = []
731
+ self._decision_cache: dict[str, tuple[AccessDecision, float]] = {}
732
+ self._lock = threading.RLock()
733
+
734
+ def add_evaluator(self, evaluator: PolicyEvaluator) -> None:
735
+ """Add a policy evaluator."""
736
+ with self._lock:
737
+ self._evaluators.append(evaluator)
738
+ # Sort by priority (descending)
739
+ self._evaluators.sort(key=lambda e: e.priority, reverse=True)
740
+
741
+ def remove_evaluator(self, name: str) -> bool:
742
+ """Remove an evaluator by name."""
743
+ with self._lock:
744
+ for i, evaluator in enumerate(self._evaluators):
745
+ if evaluator.name == name:
746
+ del self._evaluators[i]
747
+ return True
748
+ return False
749
+
750
+ def evaluate(self, context: AccessContext) -> AccessDecision:
751
+ """Evaluate access for the given context.
752
+
753
+ Args:
754
+ context: Access context containing principal, resource, action
755
+
756
+ Returns:
757
+ AccessDecision indicating whether access is allowed.
758
+ """
759
+ start_time = time.time()
760
+
761
+ # Check cache
762
+ cache_key = self._get_cache_key(context)
763
+ if self._config.cache_decisions and cache_key:
764
+ cached = self._get_cached_decision(cache_key)
765
+ if cached:
766
+ return cached
767
+
768
+ # Collect decisions from all evaluators
769
+ decisions: list[tuple[PolicyEvaluator, AccessDecision]] = []
770
+
771
+ with self._lock:
772
+ for evaluator in self._evaluators:
773
+ try:
774
+ decision = evaluator.evaluate(context)
775
+ decisions.append((evaluator, decision))
776
+ except Exception as e:
777
+ # Log error but continue with other evaluators
778
+ if self._config.log_decisions:
779
+ pass # Would log here
780
+
781
+ if not decisions:
782
+ return AccessDecision.deny("No evaluators configured")
783
+
784
+ # Combine decisions
785
+ final_decision = self._combine_decisions(decisions)
786
+ final_decision.evaluation_time_ms = (time.time() - start_time) * 1000
787
+
788
+ # Cache decision
789
+ if self._config.cache_decisions and cache_key:
790
+ self._cache_decision(cache_key, final_decision)
791
+
792
+ return final_decision
793
+
794
+ def _combine_decisions(
795
+ self,
796
+ decisions: list[tuple[PolicyEvaluator, AccessDecision]],
797
+ ) -> AccessDecision:
798
+ """Combine decisions from multiple evaluators."""
799
+ if self._config.combination == PolicyCombination.DENY_OVERRIDES:
800
+ # Any deny wins
801
+ for evaluator, decision in decisions:
802
+ if decision.allowed is False and decision.effect == PermissionEffect.DENY:
803
+ return decision
804
+
805
+ # Check for any allow
806
+ for evaluator, decision in decisions:
807
+ if decision.allowed:
808
+ return decision
809
+
810
+ elif self._config.combination == PolicyCombination.ALLOW_OVERRIDES:
811
+ # Any allow wins
812
+ for evaluator, decision in decisions:
813
+ if decision.allowed:
814
+ return decision
815
+
816
+ elif self._config.combination == PolicyCombination.FIRST_APPLICABLE:
817
+ # First definitive decision wins
818
+ for evaluator, decision in decisions:
819
+ if decision.effect in (PermissionEffect.ALLOW, PermissionEffect.DENY):
820
+ return decision
821
+
822
+ elif self._config.combination == PolicyCombination.UNANIMOUS:
823
+ # All must allow
824
+ for evaluator, decision in decisions:
825
+ if not decision.allowed:
826
+ return decision
827
+
828
+ # All allowed
829
+ _, decision = decisions[0]
830
+ return decision
831
+
832
+ # Default: deny
833
+ if self._config.deny_by_default:
834
+ return AccessDecision.deny("No matching policy (deny by default)")
835
+
836
+ return AccessDecision(
837
+ allowed=False,
838
+ reason="No definitive decision",
839
+ )
840
+
841
+ def _get_cache_key(self, context: AccessContext) -> str | None:
842
+ """Generate a cache key for the context."""
843
+ if context.principal is None:
844
+ return None
845
+
846
+ parts = [
847
+ context.principal.id,
848
+ context.resource,
849
+ context.action if isinstance(context.action, str) else context.action.value,
850
+ context.tenant_id or "",
851
+ ]
852
+ return ":".join(parts)
853
+
854
+ def _get_cached_decision(self, cache_key: str) -> AccessDecision | None:
855
+ """Get a cached decision."""
856
+ with self._lock:
857
+ if cache_key in self._decision_cache:
858
+ decision, cached_at = self._decision_cache[cache_key]
859
+ if time.time() - cached_at < self._config.cache_ttl_seconds:
860
+ return decision
861
+ else:
862
+ del self._decision_cache[cache_key]
863
+ return None
864
+
865
+ def _cache_decision(self, cache_key: str, decision: AccessDecision) -> None:
866
+ """Cache a decision."""
867
+ with self._lock:
868
+ # Limit cache size
869
+ if len(self._decision_cache) > 10000:
870
+ # Remove oldest entries
871
+ sorted_keys = sorted(
872
+ self._decision_cache.keys(),
873
+ key=lambda k: self._decision_cache[k][1],
874
+ )
875
+ for key in sorted_keys[:1000]:
876
+ del self._decision_cache[key]
877
+
878
+ self._decision_cache[cache_key] = (decision, time.time())
879
+
880
+ def invalidate_cache(
881
+ self,
882
+ principal_id: str | None = None,
883
+ resource: str | None = None,
884
+ ) -> None:
885
+ """Invalidate cached decisions."""
886
+ with self._lock:
887
+ if principal_id is None and resource is None:
888
+ self._decision_cache.clear()
889
+ return
890
+
891
+ keys_to_remove = []
892
+ for key in self._decision_cache:
893
+ parts = key.split(":")
894
+ if principal_id and parts[0] == principal_id:
895
+ keys_to_remove.append(key)
896
+ elif resource and parts[1] == resource:
897
+ keys_to_remove.append(key)
898
+
899
+ for key in keys_to_remove:
900
+ del self._decision_cache[key]
901
+
902
+ def check(
903
+ self,
904
+ principal: Principal,
905
+ resource: str,
906
+ action: str,
907
+ resource_attributes: dict[str, Any] | None = None,
908
+ ) -> AccessDecision:
909
+ """Convenience method for checking access.
910
+
911
+ Args:
912
+ principal: Principal requesting access
913
+ resource: Resource being accessed
914
+ action: Action being performed
915
+ resource_attributes: Optional resource attributes for ABAC
916
+
917
+ Returns:
918
+ AccessDecision.
919
+ """
920
+ context = AccessContext(
921
+ principal=principal,
922
+ resource=resource,
923
+ action=action,
924
+ resource_attributes=resource_attributes or {},
925
+ tenant_id=principal.tenant_id,
926
+ )
927
+ return self.evaluate(context)
928
+
929
+ def require(
930
+ self,
931
+ principal: Principal,
932
+ resource: str,
933
+ action: str,
934
+ resource_attributes: dict[str, Any] | None = None,
935
+ ) -> None:
936
+ """Check access and raise if denied.
937
+
938
+ Args:
939
+ principal: Principal requesting access
940
+ resource: Resource being accessed
941
+ action: Action being performed
942
+ resource_attributes: Optional resource attributes
943
+
944
+ Raises:
945
+ PermissionDeniedError: If access is denied.
946
+ """
947
+ decision = self.check(principal, resource, action, resource_attributes)
948
+ if not decision.allowed:
949
+ raise PermissionDeniedError(
950
+ decision.reason,
951
+ principal_id=principal.id,
952
+ resource=resource,
953
+ action=action,
954
+ )