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,2122 @@
1
+ """
2
+ Profile Visualization Module - HTML Reports for Truthound.
3
+
4
+ This module provides comprehensive HTML report generation for data profiling results,
5
+ supporting multiple visualization types, themes, and export formats.
6
+
7
+ Architecture:
8
+ - ChartRenderer: Abstract base for chart rendering (supports multiple backends)
9
+ - ReportSection: Composable report sections
10
+ - ReportTemplate: Template engine for HTML generation
11
+ - HTMLReportGenerator: Main interface for generating reports
12
+ - ReportExporter: Export to various formats (HTML, PDF, JSON)
13
+
14
+ Extensibility:
15
+ - Custom chart renderers via ChartRendererRegistry
16
+ - Custom report sections via SectionRegistry
17
+ - Custom themes via ThemeRegistry
18
+ - Plugin system for additional visualizations
19
+ """
20
+
21
+ from abc import ABC, abstractmethod
22
+ from dataclasses import dataclass, field
23
+ from enum import Enum, auto
24
+ from typing import (
25
+ Any,
26
+ Callable,
27
+ Dict,
28
+ Generic,
29
+ List,
30
+ Optional,
31
+ Protocol,
32
+ Sequence,
33
+ Tuple,
34
+ TypeVar,
35
+ Union,
36
+ runtime_checkable,
37
+ )
38
+ import base64
39
+ import html
40
+ import io
41
+ import json
42
+ import os
43
+ import threading
44
+ from datetime import datetime
45
+ from pathlib import Path
46
+
47
+
48
+ # =============================================================================
49
+ # Enums and Constants
50
+ # =============================================================================
51
+
52
+ class ChartType(Enum):
53
+ """Supported chart types."""
54
+ BAR = "bar"
55
+ HORIZONTAL_BAR = "horizontal_bar"
56
+ PIE = "pie"
57
+ DONUT = "donut"
58
+ LINE = "line"
59
+ AREA = "area"
60
+ SCATTER = "scatter"
61
+ HISTOGRAM = "histogram"
62
+ HEATMAP = "heatmap"
63
+ TABLE = "table"
64
+ GAUGE = "gauge"
65
+ SPARKLINE = "sparkline"
66
+ TREEMAP = "treemap"
67
+ SANKEY = "sankey"
68
+
69
+
70
+ class ColorScheme(Enum):
71
+ """Predefined color schemes."""
72
+ DEFAULT = "default"
73
+ CATEGORICAL = "categorical"
74
+ SEQUENTIAL = "sequential"
75
+ DIVERGING = "diverging"
76
+ QUALITATIVE = "qualitative"
77
+ TRAFFIC_LIGHT = "traffic_light"
78
+
79
+
80
+ class ReportTheme(Enum):
81
+ """Report theme options."""
82
+ LIGHT = "light"
83
+ DARK = "dark"
84
+ PROFESSIONAL = "professional"
85
+ MINIMAL = "minimal"
86
+ COLORFUL = "colorful"
87
+
88
+
89
+ class SectionType(Enum):
90
+ """Types of report sections."""
91
+ OVERVIEW = "overview"
92
+ COLUMN_DETAILS = "column_details"
93
+ DATA_QUALITY = "data_quality"
94
+ STATISTICS = "statistics"
95
+ PATTERNS = "patterns"
96
+ ALERTS = "alerts"
97
+ RECOMMENDATIONS = "recommendations"
98
+ CUSTOM = "custom"
99
+
100
+
101
+ # =============================================================================
102
+ # Color Palettes
103
+ # =============================================================================
104
+
105
+ COLOR_PALETTES: Dict[ColorScheme, List[str]] = {
106
+ ColorScheme.DEFAULT: [
107
+ "#4e79a7", "#f28e2c", "#e15759", "#76b7b2", "#59a14f",
108
+ "#edc949", "#af7aa1", "#ff9da7", "#9c755f", "#bab0ab"
109
+ ],
110
+ ColorScheme.CATEGORICAL: [
111
+ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
112
+ "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
113
+ ],
114
+ ColorScheme.SEQUENTIAL: [
115
+ "#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6",
116
+ "#4292c6", "#2171b5", "#08519c", "#08306b"
117
+ ],
118
+ ColorScheme.DIVERGING: [
119
+ "#d73027", "#f46d43", "#fdae61", "#fee090", "#ffffbf",
120
+ "#e0f3f8", "#abd9e9", "#74add1", "#4575b4"
121
+ ],
122
+ ColorScheme.QUALITATIVE: [
123
+ "#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3",
124
+ "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd"
125
+ ],
126
+ ColorScheme.TRAFFIC_LIGHT: [
127
+ "#2ecc71", "#f1c40f", "#e74c3c" # green, yellow, red
128
+ ],
129
+ }
130
+
131
+
132
+ # =============================================================================
133
+ # Data Classes
134
+ # =============================================================================
135
+
136
+ @dataclass
137
+ class ChartData:
138
+ """Data container for chart rendering."""
139
+ labels: List[str] = field(default_factory=list)
140
+ values: List[Union[int, float]] = field(default_factory=list)
141
+ series: Optional[List[Dict[str, Any]]] = None # For multi-series charts
142
+ colors: Optional[List[str]] = None
143
+ title: str = ""
144
+ subtitle: str = ""
145
+ x_label: str = ""
146
+ y_label: str = ""
147
+ show_legend: bool = True
148
+ show_values: bool = True
149
+ metadata: Dict[str, Any] = field(default_factory=dict)
150
+
151
+
152
+ @dataclass
153
+ class ChartConfig:
154
+ """Configuration for chart rendering."""
155
+ chart_type: ChartType = ChartType.BAR
156
+ width: int = 600
157
+ height: int = 400
158
+ color_scheme: ColorScheme = ColorScheme.DEFAULT
159
+ animation: bool = False
160
+ interactive: bool = True
161
+ responsive: bool = True
162
+ custom_colors: Optional[List[str]] = None
163
+ extra_options: Dict[str, Any] = field(default_factory=dict)
164
+
165
+
166
+ @dataclass
167
+ class ThemeConfig:
168
+ """Theme configuration for reports."""
169
+ name: str
170
+ background_color: str = "#ffffff"
171
+ text_color: str = "#333333"
172
+ primary_color: str = "#4e79a7"
173
+ secondary_color: str = "#f28e2c"
174
+ accent_color: str = "#e15759"
175
+ border_color: str = "#e0e0e0"
176
+ font_family: str = "system-ui, -apple-system, sans-serif"
177
+ header_bg: str = "#f8f9fa"
178
+ card_bg: str = "#ffffff"
179
+ shadow: str = "0 2px 4px rgba(0,0,0,0.1)"
180
+ border_radius: str = "8px"
181
+
182
+
183
+ @dataclass
184
+ class SectionContent:
185
+ """Content for a report section."""
186
+ section_type: SectionType
187
+ title: str
188
+ charts: List[Tuple[ChartData, ChartConfig]] = field(default_factory=list)
189
+ tables: List[Dict[str, Any]] = field(default_factory=list)
190
+ text_blocks: List[str] = field(default_factory=list)
191
+ alerts: List[Dict[str, Any]] = field(default_factory=list)
192
+ collapsible: bool = False
193
+ priority: int = 0 # Higher = more important
194
+ metadata: Dict[str, Any] = field(default_factory=dict)
195
+
196
+
197
+ @dataclass
198
+ class ReportConfig:
199
+ """Configuration for report generation."""
200
+ title: str = "Data Profile Report"
201
+ subtitle: str = ""
202
+ theme: ReportTheme = ReportTheme.LIGHT
203
+ logo_path: Optional[str] = None
204
+ logo_base64: Optional[str] = None
205
+ include_toc: bool = True
206
+ include_timestamp: bool = True
207
+ include_summary: bool = True
208
+ sections: List[SectionType] = field(default_factory=lambda: [
209
+ SectionType.OVERVIEW,
210
+ SectionType.DATA_QUALITY,
211
+ SectionType.COLUMN_DETAILS,
212
+ SectionType.PATTERNS,
213
+ SectionType.RECOMMENDATIONS,
214
+ ])
215
+ custom_css: Optional[str] = None
216
+ custom_js: Optional[str] = None
217
+ embed_resources: bool = True
218
+ language: str = "en"
219
+
220
+
221
+ @dataclass
222
+ class ProfileData:
223
+ """Container for profile data to be visualized."""
224
+ table_name: str
225
+ row_count: int
226
+ column_count: int
227
+ columns: List[Dict[str, Any]] # Column profiles
228
+ quality_scores: Optional[Dict[str, float]] = None
229
+ patterns_found: Optional[List[Dict[str, Any]]] = None
230
+ alerts: Optional[List[Dict[str, Any]]] = None
231
+ recommendations: Optional[List[str]] = None
232
+ timestamp: Optional[datetime] = None
233
+ metadata: Dict[str, Any] = field(default_factory=dict)
234
+
235
+
236
+ # =============================================================================
237
+ # Theme Definitions
238
+ # =============================================================================
239
+
240
+ THEME_CONFIGS: Dict[ReportTheme, ThemeConfig] = {
241
+ ReportTheme.LIGHT: ThemeConfig(
242
+ name="light",
243
+ background_color="#ffffff",
244
+ text_color="#333333",
245
+ primary_color="#4e79a7",
246
+ secondary_color="#f28e2c",
247
+ accent_color="#e15759",
248
+ border_color="#e0e0e0",
249
+ header_bg="#f8f9fa",
250
+ card_bg="#ffffff",
251
+ ),
252
+ ReportTheme.DARK: ThemeConfig(
253
+ name="dark",
254
+ background_color="#1a1a2e",
255
+ text_color="#eaeaea",
256
+ primary_color="#64b5f6",
257
+ secondary_color="#ffb74d",
258
+ accent_color="#ef5350",
259
+ border_color="#333355",
260
+ header_bg="#16213e",
261
+ card_bg="#0f3460",
262
+ shadow="0 2px 4px rgba(0,0,0,0.3)",
263
+ ),
264
+ ReportTheme.PROFESSIONAL: ThemeConfig(
265
+ name="professional",
266
+ background_color="#f5f5f5",
267
+ text_color="#2c3e50",
268
+ primary_color="#2c3e50",
269
+ secondary_color="#3498db",
270
+ accent_color="#e74c3c",
271
+ border_color="#bdc3c7",
272
+ header_bg="#ecf0f1",
273
+ card_bg="#ffffff",
274
+ font_family="'Segoe UI', Tahoma, Geneva, sans-serif",
275
+ border_radius="4px",
276
+ ),
277
+ ReportTheme.MINIMAL: ThemeConfig(
278
+ name="minimal",
279
+ background_color="#ffffff",
280
+ text_color="#000000",
281
+ primary_color="#000000",
282
+ secondary_color="#666666",
283
+ accent_color="#333333",
284
+ border_color="#cccccc",
285
+ header_bg="#ffffff",
286
+ card_bg="#ffffff",
287
+ shadow="none",
288
+ border_radius="0",
289
+ ),
290
+ ReportTheme.COLORFUL: ThemeConfig(
291
+ name="colorful",
292
+ background_color="#fafafa",
293
+ text_color="#333333",
294
+ primary_color="#6c5ce7",
295
+ secondary_color="#00b894",
296
+ accent_color="#fd79a8",
297
+ border_color="#dfe6e9",
298
+ header_bg="#ffffff",
299
+ card_bg="#ffffff",
300
+ border_radius="12px",
301
+ ),
302
+ }
303
+
304
+
305
+ # =============================================================================
306
+ # Abstract Base Classes and Protocols
307
+ # =============================================================================
308
+
309
+ @runtime_checkable
310
+ class ChartRendererProtocol(Protocol):
311
+ """Protocol for chart renderers."""
312
+
313
+ def render(self, data: ChartData, config: ChartConfig) -> str:
314
+ """Render chart to HTML/SVG string."""
315
+ ...
316
+
317
+ def supports_chart_type(self, chart_type: ChartType) -> bool:
318
+ """Check if renderer supports the chart type."""
319
+ ...
320
+
321
+
322
+ class ChartRenderer(ABC):
323
+ """Abstract base class for chart rendering."""
324
+
325
+ @abstractmethod
326
+ def render(self, data: ChartData, config: ChartConfig) -> str:
327
+ """Render chart to HTML/SVG string."""
328
+ pass
329
+
330
+ @abstractmethod
331
+ def supports_chart_type(self, chart_type: ChartType) -> bool:
332
+ """Check if renderer supports the chart type."""
333
+ pass
334
+
335
+ def get_colors(self, config: ChartConfig, count: int) -> List[str]:
336
+ """Get colors for the chart."""
337
+ if config.custom_colors:
338
+ return config.custom_colors[:count]
339
+ palette = COLOR_PALETTES.get(config.color_scheme, COLOR_PALETTES[ColorScheme.DEFAULT])
340
+ # Cycle through palette if needed
341
+ return [palette[i % len(palette)] for i in range(count)]
342
+
343
+
344
+ class SectionRenderer(ABC):
345
+ """Abstract base class for section rendering."""
346
+
347
+ @abstractmethod
348
+ def render(
349
+ self,
350
+ content: SectionContent,
351
+ chart_renderer: ChartRenderer,
352
+ theme: ThemeConfig,
353
+ ) -> str:
354
+ """Render section to HTML string."""
355
+ pass
356
+
357
+ @abstractmethod
358
+ def get_section_type(self) -> SectionType:
359
+ """Get the section type this renderer handles."""
360
+ pass
361
+
362
+
363
+ # =============================================================================
364
+ # Registries
365
+ # =============================================================================
366
+
367
+ class ChartRendererRegistry:
368
+ """Registry for chart renderers."""
369
+
370
+ _instance: Optional["ChartRendererRegistry"] = None
371
+ _lock: threading.Lock = threading.Lock()
372
+
373
+ def __new__(cls) -> "ChartRendererRegistry":
374
+ if cls._instance is None:
375
+ with cls._lock:
376
+ if cls._instance is None:
377
+ cls._instance = super().__new__(cls)
378
+ cls._instance._renderers: Dict[str, ChartRenderer] = {}
379
+ cls._instance._default: Optional[str] = None
380
+ return cls._instance
381
+
382
+ def register(self, name: str, renderer: ChartRenderer, default: bool = False) -> None:
383
+ """Register a chart renderer."""
384
+ self._renderers[name] = renderer
385
+ if default or self._default is None:
386
+ self._default = name
387
+
388
+ def get(self, name: Optional[str] = None) -> Optional[ChartRenderer]:
389
+ """Get a chart renderer by name or default."""
390
+ if name is None:
391
+ name = self._default
392
+ return self._renderers.get(name) if name else None
393
+
394
+ def list_renderers(self) -> List[str]:
395
+ """List all registered renderer names."""
396
+ return list(self._renderers.keys())
397
+
398
+ def clear(self) -> None:
399
+ """Clear all registered renderers."""
400
+ self._renderers.clear()
401
+ self._default = None
402
+
403
+
404
+ class SectionRegistry:
405
+ """Registry for section renderers."""
406
+
407
+ _instance: Optional["SectionRegistry"] = None
408
+ _lock: threading.Lock = threading.Lock()
409
+
410
+ def __new__(cls) -> "SectionRegistry":
411
+ if cls._instance is None:
412
+ with cls._lock:
413
+ if cls._instance is None:
414
+ cls._instance = super().__new__(cls)
415
+ cls._instance._renderers: Dict[SectionType, SectionRenderer] = {}
416
+ return cls._instance
417
+
418
+ def register(self, renderer: SectionRenderer) -> None:
419
+ """Register a section renderer."""
420
+ self._renderers[renderer.get_section_type()] = renderer
421
+
422
+ def get(self, section_type: SectionType) -> Optional[SectionRenderer]:
423
+ """Get a section renderer by type."""
424
+ return self._renderers.get(section_type)
425
+
426
+ def list_sections(self) -> List[SectionType]:
427
+ """List all registered section types."""
428
+ return list(self._renderers.keys())
429
+
430
+ def clear(self) -> None:
431
+ """Clear all registered section renderers."""
432
+ self._renderers.clear()
433
+
434
+
435
+ class ThemeRegistry:
436
+ """Registry for custom themes."""
437
+
438
+ _instance: Optional["ThemeRegistry"] = None
439
+ _lock: threading.Lock = threading.Lock()
440
+
441
+ def __new__(cls) -> "ThemeRegistry":
442
+ if cls._instance is None:
443
+ with cls._lock:
444
+ if cls._instance is None:
445
+ cls._instance = super().__new__(cls)
446
+ cls._instance._themes: Dict[str, ThemeConfig] = {}
447
+ # Register built-in themes
448
+ for theme_enum, config in THEME_CONFIGS.items():
449
+ cls._instance._themes[theme_enum.value] = config
450
+ return cls._instance
451
+
452
+ def register(self, name: str, theme: ThemeConfig) -> None:
453
+ """Register a custom theme."""
454
+ self._themes[name] = theme
455
+
456
+ def get(self, name: Union[str, ReportTheme]) -> ThemeConfig:
457
+ """Get a theme by name."""
458
+ if isinstance(name, ReportTheme):
459
+ name = name.value
460
+ return self._themes.get(name, THEME_CONFIGS[ReportTheme.LIGHT])
461
+
462
+ def list_themes(self) -> List[str]:
463
+ """List all registered theme names."""
464
+ return list(self._themes.keys())
465
+
466
+
467
+ # Singleton instances
468
+ chart_renderer_registry = ChartRendererRegistry()
469
+ section_registry = SectionRegistry()
470
+ theme_registry = ThemeRegistry()
471
+
472
+
473
+ # =============================================================================
474
+ # Built-in Chart Renderers
475
+ # =============================================================================
476
+
477
+ class SVGChartRenderer(ChartRenderer):
478
+ """Pure SVG chart renderer - no external dependencies."""
479
+
480
+ SUPPORTED_TYPES = {
481
+ ChartType.BAR,
482
+ ChartType.HORIZONTAL_BAR,
483
+ ChartType.PIE,
484
+ ChartType.DONUT,
485
+ ChartType.LINE,
486
+ ChartType.HISTOGRAM,
487
+ ChartType.GAUGE,
488
+ ChartType.SPARKLINE,
489
+ ChartType.TABLE,
490
+ }
491
+
492
+ def supports_chart_type(self, chart_type: ChartType) -> bool:
493
+ return chart_type in self.SUPPORTED_TYPES
494
+
495
+ def render(self, data: ChartData, config: ChartConfig) -> str:
496
+ """Render chart to SVG string."""
497
+ if config.chart_type == ChartType.BAR:
498
+ return self._render_bar(data, config)
499
+ elif config.chart_type == ChartType.HORIZONTAL_BAR:
500
+ return self._render_horizontal_bar(data, config)
501
+ elif config.chart_type == ChartType.PIE:
502
+ return self._render_pie(data, config, donut=False)
503
+ elif config.chart_type == ChartType.DONUT:
504
+ return self._render_pie(data, config, donut=True)
505
+ elif config.chart_type == ChartType.LINE:
506
+ return self._render_line(data, config)
507
+ elif config.chart_type == ChartType.HISTOGRAM:
508
+ return self._render_histogram(data, config)
509
+ elif config.chart_type == ChartType.GAUGE:
510
+ return self._render_gauge(data, config)
511
+ elif config.chart_type == ChartType.SPARKLINE:
512
+ return self._render_sparkline(data, config)
513
+ elif config.chart_type == ChartType.TABLE:
514
+ return self._render_table(data, config)
515
+ else:
516
+ return f"<p>Chart type {config.chart_type.value} not supported by SVG renderer</p>"
517
+
518
+ def _svg_header(self, config: ChartConfig) -> str:
519
+ """Generate SVG header with proper attributes."""
520
+ responsive = 'viewBox="0 0 {w} {h}" preserveAspectRatio="xMidYMid meet"'.format(
521
+ w=config.width, h=config.height
522
+ ) if config.responsive else f'width="{config.width}" height="{config.height}"'
523
+
524
+ return f'''<svg xmlns="http://www.w3.org/2000/svg" {responsive}
525
+ style="max-width: 100%; height: auto;">'''
526
+
527
+ def _render_bar(self, data: ChartData, config: ChartConfig) -> str:
528
+ """Render vertical bar chart."""
529
+ if not data.values:
530
+ return "<p>No data available</p>"
531
+
532
+ w, h = config.width, config.height
533
+ margin = {"top": 40, "right": 20, "bottom": 60, "left": 60}
534
+ chart_w = w - margin["left"] - margin["right"]
535
+ chart_h = h - margin["top"] - margin["bottom"]
536
+
537
+ colors = self.get_colors(config, len(data.values))
538
+ max_val = max(data.values) if data.values else 1
539
+ bar_width = chart_w / len(data.values) * 0.8
540
+ bar_gap = chart_w / len(data.values) * 0.2
541
+
542
+ svg = [self._svg_header(config)]
543
+
544
+ # Title
545
+ if data.title:
546
+ svg.append(f'<text x="{w/2}" y="25" text-anchor="middle" '
547
+ f'font-size="16" font-weight="bold">{html.escape(data.title)}</text>')
548
+
549
+ # Y-axis
550
+ svg.append(f'<line x1="{margin["left"]}" y1="{margin["top"]}" '
551
+ f'x2="{margin["left"]}" y2="{h - margin["bottom"]}" '
552
+ f'stroke="#ccc" stroke-width="1"/>')
553
+
554
+ # X-axis
555
+ svg.append(f'<line x1="{margin["left"]}" y1="{h - margin["bottom"]}" '
556
+ f'x2="{w - margin["right"]}" y2="{h - margin["bottom"]}" '
557
+ f'stroke="#ccc" stroke-width="1"/>')
558
+
559
+ # Bars
560
+ for i, (label, value) in enumerate(zip(data.labels, data.values)):
561
+ x = margin["left"] + i * (bar_width + bar_gap) + bar_gap / 2
562
+ bar_h = (value / max_val) * chart_h if max_val > 0 else 0
563
+ y = h - margin["bottom"] - bar_h
564
+
565
+ # Bar
566
+ svg.append(f'<rect x="{x}" y="{y}" width="{bar_width}" height="{bar_h}" '
567
+ f'fill="{colors[i]}" rx="2">'
568
+ f'<title>{html.escape(label)}: {value}</title></rect>')
569
+
570
+ # Value label
571
+ if data.show_values:
572
+ svg.append(f'<text x="{x + bar_width/2}" y="{y - 5}" '
573
+ f'text-anchor="middle" font-size="11">{value:.1f}</text>')
574
+
575
+ # X-axis label
576
+ label_y = h - margin["bottom"] + 15
577
+ svg.append(f'<text x="{x + bar_width/2}" y="{label_y}" '
578
+ f'text-anchor="middle" font-size="10" '
579
+ f'transform="rotate(-45 {x + bar_width/2} {label_y})">'
580
+ f'{html.escape(str(label)[:15])}</text>')
581
+
582
+ svg.append('</svg>')
583
+ return '\n'.join(svg)
584
+
585
+ def _render_horizontal_bar(self, data: ChartData, config: ChartConfig) -> str:
586
+ """Render horizontal bar chart."""
587
+ if not data.values:
588
+ return "<p>No data available</p>"
589
+
590
+ w, h = config.width, config.height
591
+ margin = {"top": 40, "right": 60, "bottom": 30, "left": 120}
592
+ chart_w = w - margin["left"] - margin["right"]
593
+ chart_h = h - margin["top"] - margin["bottom"]
594
+
595
+ colors = self.get_colors(config, len(data.values))
596
+ max_val = max(data.values) if data.values else 1
597
+ bar_height = chart_h / len(data.values) * 0.8
598
+ bar_gap = chart_h / len(data.values) * 0.2
599
+
600
+ svg = [self._svg_header(config)]
601
+
602
+ # Title
603
+ if data.title:
604
+ svg.append(f'<text x="{w/2}" y="25" text-anchor="middle" '
605
+ f'font-size="16" font-weight="bold">{html.escape(data.title)}</text>')
606
+
607
+ # Bars
608
+ for i, (label, value) in enumerate(zip(data.labels, data.values)):
609
+ y = margin["top"] + i * (bar_height + bar_gap) + bar_gap / 2
610
+ bar_w = (value / max_val) * chart_w if max_val > 0 else 0
611
+
612
+ # Label
613
+ svg.append(f'<text x="{margin["left"] - 5}" y="{y + bar_height/2 + 4}" '
614
+ f'text-anchor="end" font-size="11">{html.escape(str(label)[:20])}</text>')
615
+
616
+ # Bar
617
+ svg.append(f'<rect x="{margin["left"]}" y="{y}" width="{bar_w}" height="{bar_height}" '
618
+ f'fill="{colors[i]}" rx="2">'
619
+ f'<title>{html.escape(label)}: {value}</title></rect>')
620
+
621
+ # Value
622
+ if data.show_values:
623
+ svg.append(f'<text x="{margin["left"] + bar_w + 5}" y="{y + bar_height/2 + 4}" '
624
+ f'font-size="11">{value:.1f}</text>')
625
+
626
+ svg.append('</svg>')
627
+ return '\n'.join(svg)
628
+
629
+ def _render_pie(self, data: ChartData, config: ChartConfig, donut: bool = False) -> str:
630
+ """Render pie or donut chart."""
631
+ if not data.values:
632
+ return "<p>No data available</p>"
633
+
634
+ import math
635
+
636
+ w, h = config.width, config.height
637
+ cx, cy = w / 2, h / 2
638
+ radius = min(w, h) / 2 - 40
639
+ inner_radius = radius * 0.6 if donut else 0
640
+
641
+ colors = self.get_colors(config, len(data.values))
642
+ total = sum(data.values) if data.values else 1
643
+
644
+ svg = [self._svg_header(config)]
645
+
646
+ # Title
647
+ if data.title:
648
+ svg.append(f'<text x="{w/2}" y="25" text-anchor="middle" '
649
+ f'font-size="16" font-weight="bold">{html.escape(data.title)}</text>')
650
+
651
+ # Slices
652
+ start_angle = -math.pi / 2
653
+ for i, (label, value) in enumerate(zip(data.labels, data.values)):
654
+ if value <= 0:
655
+ continue
656
+
657
+ angle = (value / total) * 2 * math.pi
658
+ end_angle = start_angle + angle
659
+
660
+ # Arc path
661
+ large_arc = 1 if angle > math.pi else 0
662
+
663
+ # Outer arc
664
+ x1 = cx + radius * math.cos(start_angle)
665
+ y1 = cy + radius * math.sin(start_angle)
666
+ x2 = cx + radius * math.cos(end_angle)
667
+ y2 = cy + radius * math.sin(end_angle)
668
+
669
+ if donut:
670
+ # Inner arc
671
+ ix1 = cx + inner_radius * math.cos(start_angle)
672
+ iy1 = cy + inner_radius * math.sin(start_angle)
673
+ ix2 = cx + inner_radius * math.cos(end_angle)
674
+ iy2 = cy + inner_radius * math.sin(end_angle)
675
+
676
+ path = (f'M {ix1} {iy1} L {x1} {y1} '
677
+ f'A {radius} {radius} 0 {large_arc} 1 {x2} {y2} '
678
+ f'L {ix2} {iy2} '
679
+ f'A {inner_radius} {inner_radius} 0 {large_arc} 0 {ix1} {iy1} Z')
680
+ else:
681
+ path = (f'M {cx} {cy} L {x1} {y1} '
682
+ f'A {radius} {radius} 0 {large_arc} 1 {x2} {y2} Z')
683
+
684
+ pct = (value / total) * 100
685
+ svg.append(f'<path d="{path}" fill="{colors[i]}">'
686
+ f'<title>{html.escape(label)}: {value} ({pct:.1f}%)</title></path>')
687
+
688
+ # Label
689
+ if pct > 5:
690
+ label_angle = start_angle + angle / 2
691
+ label_radius = radius * 0.7 if not donut else (radius + inner_radius) / 2
692
+ lx = cx + label_radius * math.cos(label_angle)
693
+ ly = cy + label_radius * math.sin(label_angle)
694
+ svg.append(f'<text x="{lx}" y="{ly}" text-anchor="middle" '
695
+ f'font-size="10" fill="white">{pct:.1f}%</text>')
696
+
697
+ start_angle = end_angle
698
+
699
+ # Legend
700
+ if data.show_legend:
701
+ legend_x = w - 100
702
+ for i, label in enumerate(data.labels[:8]): # Limit legend items
703
+ ly = 50 + i * 18
704
+ svg.append(f'<rect x="{legend_x}" y="{ly}" width="12" height="12" fill="{colors[i]}"/>')
705
+ svg.append(f'<text x="{legend_x + 16}" y="{ly + 10}" font-size="10">'
706
+ f'{html.escape(str(label)[:12])}</text>')
707
+
708
+ svg.append('</svg>')
709
+ return '\n'.join(svg)
710
+
711
+ def _render_line(self, data: ChartData, config: ChartConfig) -> str:
712
+ """Render line chart."""
713
+ if not data.values:
714
+ return "<p>No data available</p>"
715
+
716
+ w, h = config.width, config.height
717
+ margin = {"top": 40, "right": 20, "bottom": 60, "left": 60}
718
+ chart_w = w - margin["left"] - margin["right"]
719
+ chart_h = h - margin["top"] - margin["bottom"]
720
+
721
+ colors = self.get_colors(config, 1)
722
+ max_val = max(data.values) if data.values else 1
723
+ min_val = min(data.values) if data.values else 0
724
+ val_range = max_val - min_val if max_val != min_val else 1
725
+
726
+ svg = [self._svg_header(config)]
727
+
728
+ # Title
729
+ if data.title:
730
+ svg.append(f'<text x="{w/2}" y="25" text-anchor="middle" '
731
+ f'font-size="16" font-weight="bold">{html.escape(data.title)}</text>')
732
+
733
+ # Grid and axes
734
+ svg.append(f'<line x1="{margin["left"]}" y1="{margin["top"]}" '
735
+ f'x2="{margin["left"]}" y2="{h - margin["bottom"]}" stroke="#ccc"/>')
736
+ svg.append(f'<line x1="{margin["left"]}" y1="{h - margin["bottom"]}" '
737
+ f'x2="{w - margin["right"]}" y2="{h - margin["bottom"]}" stroke="#ccc"/>')
738
+
739
+ # Build path
740
+ points = []
741
+ step = chart_w / (len(data.values) - 1) if len(data.values) > 1 else 0
742
+
743
+ for i, value in enumerate(data.values):
744
+ x = margin["left"] + i * step
745
+ y = h - margin["bottom"] - ((value - min_val) / val_range) * chart_h
746
+ points.append(f'{x},{y}')
747
+
748
+ # Line
749
+ svg.append(f'<polyline points="{" ".join(points)}" '
750
+ f'fill="none" stroke="{colors[0]}" stroke-width="2"/>')
751
+
752
+ # Points
753
+ for i, (value, point) in enumerate(zip(data.values, points)):
754
+ x, y = point.split(',')
755
+ svg.append(f'<circle cx="{x}" cy="{y}" r="4" fill="{colors[0]}">'
756
+ f'<title>{data.labels[i] if i < len(data.labels) else i}: {value}</title></circle>')
757
+
758
+ svg.append('</svg>')
759
+ return '\n'.join(svg)
760
+
761
+ def _render_histogram(self, data: ChartData, config: ChartConfig) -> str:
762
+ """Render histogram (bar chart with no gaps)."""
763
+ if not data.values:
764
+ return "<p>No data available</p>"
765
+
766
+ w, h = config.width, config.height
767
+ margin = {"top": 40, "right": 20, "bottom": 60, "left": 60}
768
+ chart_w = w - margin["left"] - margin["right"]
769
+ chart_h = h - margin["top"] - margin["bottom"]
770
+
771
+ colors = self.get_colors(config, 1)
772
+ max_val = max(data.values) if data.values else 1
773
+ bar_width = chart_w / len(data.values)
774
+
775
+ svg = [self._svg_header(config)]
776
+
777
+ # Title
778
+ if data.title:
779
+ svg.append(f'<text x="{w/2}" y="25" text-anchor="middle" '
780
+ f'font-size="16" font-weight="bold">{html.escape(data.title)}</text>')
781
+
782
+ # Axes
783
+ svg.append(f'<line x1="{margin["left"]}" y1="{h - margin["bottom"]}" '
784
+ f'x2="{w - margin["right"]}" y2="{h - margin["bottom"]}" stroke="#ccc"/>')
785
+
786
+ # Bars
787
+ for i, value in enumerate(data.values):
788
+ x = margin["left"] + i * bar_width
789
+ bar_h = (value / max_val) * chart_h if max_val > 0 else 0
790
+ y = h - margin["bottom"] - bar_h
791
+
792
+ label = data.labels[i] if i < len(data.labels) else str(i)
793
+ svg.append(f'<rect x="{x}" y="{y}" width="{bar_width}" height="{bar_h}" '
794
+ f'fill="{colors[0]}" stroke="white" stroke-width="0.5">'
795
+ f'<title>{html.escape(label)}: {value}</title></rect>')
796
+
797
+ svg.append('</svg>')
798
+ return '\n'.join(svg)
799
+
800
+ def _render_gauge(self, data: ChartData, config: ChartConfig) -> str:
801
+ """Render gauge chart."""
802
+ import math
803
+
804
+ if not data.values:
805
+ return "<p>No data available</p>"
806
+
807
+ value = data.values[0]
808
+ max_val = data.metadata.get("max", 100)
809
+ min_val = data.metadata.get("min", 0)
810
+
811
+ w, h = config.width, min(config.height, config.width * 0.6)
812
+ cx, cy = w / 2, h - 30
813
+ radius = min(w, h * 1.5) / 2 - 40
814
+
815
+ # Calculate angle (180 degrees for gauge)
816
+ ratio = (value - min_val) / (max_val - min_val) if max_val != min_val else 0
817
+ ratio = max(0, min(1, ratio))
818
+ angle = math.pi * (1 - ratio)
819
+
820
+ # Color based on value
821
+ if ratio < 0.33:
822
+ color = COLOR_PALETTES[ColorScheme.TRAFFIC_LIGHT][2] # Red
823
+ elif ratio < 0.67:
824
+ color = COLOR_PALETTES[ColorScheme.TRAFFIC_LIGHT][1] # Yellow
825
+ else:
826
+ color = COLOR_PALETTES[ColorScheme.TRAFFIC_LIGHT][0] # Green
827
+
828
+ svg = [self._svg_header(config)]
829
+
830
+ # Title
831
+ if data.title:
832
+ svg.append(f'<text x="{w/2}" y="25" text-anchor="middle" '
833
+ f'font-size="16" font-weight="bold">{html.escape(data.title)}</text>')
834
+
835
+ # Background arc
836
+ svg.append(f'<path d="M {cx - radius} {cy} A {radius} {radius} 0 0 1 {cx + radius} {cy}" '
837
+ f'fill="none" stroke="#e0e0e0" stroke-width="20" stroke-linecap="round"/>')
838
+
839
+ # Value arc
840
+ end_x = cx + radius * math.cos(angle)
841
+ end_y = cy - radius * math.sin(angle)
842
+ large_arc = 1 if ratio > 0.5 else 0
843
+
844
+ svg.append(f'<path d="M {cx - radius} {cy} A {radius} {radius} 0 {large_arc} 1 {end_x} {end_y}" '
845
+ f'fill="none" stroke="{color}" stroke-width="20" stroke-linecap="round"/>')
846
+
847
+ # Needle
848
+ needle_x = cx + (radius - 30) * math.cos(angle)
849
+ needle_y = cy - (radius - 30) * math.sin(angle)
850
+ svg.append(f'<line x1="{cx}" y1="{cy}" x2="{needle_x}" y2="{needle_y}" '
851
+ f'stroke="#333" stroke-width="3" stroke-linecap="round"/>')
852
+ svg.append(f'<circle cx="{cx}" cy="{cy}" r="8" fill="#333"/>')
853
+
854
+ # Value text
855
+ svg.append(f'<text x="{cx}" y="{cy - 20}" text-anchor="middle" '
856
+ f'font-size="24" font-weight="bold">{value:.1f}</text>')
857
+
858
+ # Min/Max labels
859
+ svg.append(f'<text x="{cx - radius}" y="{cy + 20}" text-anchor="middle" '
860
+ f'font-size="11">{min_val}</text>')
861
+ svg.append(f'<text x="{cx + radius}" y="{cy + 20}" text-anchor="middle" '
862
+ f'font-size="11">{max_val}</text>')
863
+
864
+ svg.append('</svg>')
865
+ return '\n'.join(svg)
866
+
867
+ def _render_sparkline(self, data: ChartData, config: ChartConfig) -> str:
868
+ """Render sparkline (mini line chart)."""
869
+ if not data.values:
870
+ return ""
871
+
872
+ w = config.width
873
+ h = config.height
874
+
875
+ colors = self.get_colors(config, 1)
876
+ max_val = max(data.values)
877
+ min_val = min(data.values)
878
+ val_range = max_val - min_val if max_val != min_val else 1
879
+
880
+ step = w / (len(data.values) - 1) if len(data.values) > 1 else 0
881
+
882
+ points = []
883
+ for i, value in enumerate(data.values):
884
+ x = i * step
885
+ y = h - ((value - min_val) / val_range) * h
886
+ points.append(f'{x},{y}')
887
+
888
+ return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}">
889
+ <polyline points="{" ".join(points)}" fill="none" stroke="{colors[0]}" stroke-width="1.5"/>
890
+ </svg>'''
891
+
892
+ def _render_table(self, data: ChartData, config: ChartConfig) -> str:
893
+ """Render data as HTML table."""
894
+ if not data.labels and not data.values:
895
+ return "<p>No data available</p>"
896
+
897
+ html_parts = ['<table class="data-table">']
898
+
899
+ # Header
900
+ if data.metadata.get("headers"):
901
+ html_parts.append('<thead><tr>')
902
+ for header in data.metadata["headers"]:
903
+ html_parts.append(f'<th>{html.escape(str(header))}</th>')
904
+ html_parts.append('</tr></thead>')
905
+
906
+ # Body
907
+ html_parts.append('<tbody>')
908
+ rows = data.metadata.get("rows", [])
909
+ if rows:
910
+ for row in rows:
911
+ html_parts.append('<tr>')
912
+ for cell in row:
913
+ html_parts.append(f'<td>{html.escape(str(cell))}</td>')
914
+ html_parts.append('</tr>')
915
+ else:
916
+ # Use labels/values as simple two-column table
917
+ for label, value in zip(data.labels, data.values):
918
+ html_parts.append(f'<tr><td>{html.escape(str(label))}</td>'
919
+ f'<td>{value}</td></tr>')
920
+
921
+ html_parts.append('</tbody></table>')
922
+ return '\n'.join(html_parts)
923
+
924
+
925
+ # =============================================================================
926
+ # Section Renderers
927
+ # =============================================================================
928
+
929
+ class BaseSectionRenderer(SectionRenderer):
930
+ """Base class for section renderers with common functionality."""
931
+
932
+ def render_charts(
933
+ self,
934
+ charts: List[Tuple[ChartData, ChartConfig]],
935
+ chart_renderer: ChartRenderer,
936
+ ) -> str:
937
+ """Render all charts in the section."""
938
+ html_parts = []
939
+ for data, config in charts:
940
+ if chart_renderer.supports_chart_type(config.chart_type):
941
+ chart_html = chart_renderer.render(data, config)
942
+ html_parts.append(f'<div class="chart-container">{chart_html}</div>')
943
+ return '\n'.join(html_parts)
944
+
945
+ def render_tables(self, tables: List[Dict[str, Any]]) -> str:
946
+ """Render all tables in the section."""
947
+ html_parts = []
948
+ for table in tables:
949
+ html_parts.append('<table class="data-table">')
950
+
951
+ # Headers
952
+ if "headers" in table:
953
+ html_parts.append('<thead><tr>')
954
+ for header in table["headers"]:
955
+ html_parts.append(f'<th>{html.escape(str(header))}</th>')
956
+ html_parts.append('</tr></thead>')
957
+
958
+ # Rows
959
+ html_parts.append('<tbody>')
960
+ for row in table.get("rows", []):
961
+ html_parts.append('<tr>')
962
+ for cell in row:
963
+ html_parts.append(f'<td>{html.escape(str(cell))}</td>')
964
+ html_parts.append('</tr>')
965
+ html_parts.append('</tbody></table>')
966
+
967
+ return '\n'.join(html_parts)
968
+
969
+ def render_alerts(self, alerts: List[Dict[str, Any]]) -> str:
970
+ """Render alert boxes."""
971
+ html_parts = []
972
+ for alert in alerts:
973
+ level = alert.get("level", "info")
974
+ message = alert.get("message", "")
975
+ html_parts.append(f'<div class="alert alert-{level}">{html.escape(message)}</div>')
976
+ return '\n'.join(html_parts)
977
+
978
+
979
+ class OverviewSectionRenderer(BaseSectionRenderer):
980
+ """Renderer for overview section."""
981
+
982
+ def get_section_type(self) -> SectionType:
983
+ return SectionType.OVERVIEW
984
+
985
+ def render(
986
+ self,
987
+ content: SectionContent,
988
+ chart_renderer: ChartRenderer,
989
+ theme: ThemeConfig,
990
+ ) -> str:
991
+ html_parts = [
992
+ f'<section class="report-section section-overview" id="section-overview">',
993
+ f'<h2>{html.escape(content.title)}</h2>',
994
+ '<div class="section-content">',
995
+ ]
996
+
997
+ # Text blocks (summary info)
998
+ for text in content.text_blocks:
999
+ html_parts.append(f'<p>{text}</p>')
1000
+
1001
+ # Charts (usually pie/donut for data types, gauges for quality)
1002
+ html_parts.append('<div class="charts-grid">')
1003
+ html_parts.append(self.render_charts(content.charts, chart_renderer))
1004
+ html_parts.append('</div>')
1005
+
1006
+ # Tables (summary statistics)
1007
+ html_parts.append(self.render_tables(content.tables))
1008
+
1009
+ html_parts.extend(['</div>', '</section>'])
1010
+ return '\n'.join(html_parts)
1011
+
1012
+
1013
+ class DataQualitySectionRenderer(BaseSectionRenderer):
1014
+ """Renderer for data quality section."""
1015
+
1016
+ def get_section_type(self) -> SectionType:
1017
+ return SectionType.DATA_QUALITY
1018
+
1019
+ def render(
1020
+ self,
1021
+ content: SectionContent,
1022
+ chart_renderer: ChartRenderer,
1023
+ theme: ThemeConfig,
1024
+ ) -> str:
1025
+ html_parts = [
1026
+ f'<section class="report-section section-quality" id="section-quality">',
1027
+ f'<h2>{html.escape(content.title)}</h2>',
1028
+ '<div class="section-content">',
1029
+ ]
1030
+
1031
+ # Alerts first (important issues)
1032
+ if content.alerts:
1033
+ html_parts.append('<div class="alerts-container">')
1034
+ html_parts.append(self.render_alerts(content.alerts))
1035
+ html_parts.append('</div>')
1036
+
1037
+ # Quality gauges
1038
+ html_parts.append('<div class="quality-gauges">')
1039
+ html_parts.append(self.render_charts(content.charts, chart_renderer))
1040
+ html_parts.append('</div>')
1041
+
1042
+ # Detailed tables
1043
+ html_parts.append(self.render_tables(content.tables))
1044
+
1045
+ html_parts.extend(['</div>', '</section>'])
1046
+ return '\n'.join(html_parts)
1047
+
1048
+
1049
+ class ColumnDetailsSectionRenderer(BaseSectionRenderer):
1050
+ """Renderer for column details section."""
1051
+
1052
+ def get_section_type(self) -> SectionType:
1053
+ return SectionType.COLUMN_DETAILS
1054
+
1055
+ def render(
1056
+ self,
1057
+ content: SectionContent,
1058
+ chart_renderer: ChartRenderer,
1059
+ theme: ThemeConfig,
1060
+ ) -> str:
1061
+ collapsible_attr = 'class="collapsible"' if content.collapsible else ''
1062
+ html_parts = [
1063
+ f'<section class="report-section section-columns" id="section-columns" {collapsible_attr}>',
1064
+ f'<h2>{html.escape(content.title)}</h2>',
1065
+ '<div class="section-content">',
1066
+ ]
1067
+
1068
+ # Column cards
1069
+ for i, (data, config) in enumerate(content.charts):
1070
+ column_name = data.title or f"Column {i + 1}"
1071
+ html_parts.append(f'''
1072
+ <div class="column-card">
1073
+ <h3>{html.escape(column_name)}</h3>
1074
+ <div class="column-chart">
1075
+ {chart_renderer.render(data, config) if chart_renderer.supports_chart_type(config.chart_type) else ''}
1076
+ </div>
1077
+ </div>
1078
+ ''')
1079
+
1080
+ # Summary table
1081
+ html_parts.append(self.render_tables(content.tables))
1082
+
1083
+ html_parts.extend(['</div>', '</section>'])
1084
+ return '\n'.join(html_parts)
1085
+
1086
+
1087
+ class PatternsSectionRenderer(BaseSectionRenderer):
1088
+ """Renderer for patterns section."""
1089
+
1090
+ def get_section_type(self) -> SectionType:
1091
+ return SectionType.PATTERNS
1092
+
1093
+ def render(
1094
+ self,
1095
+ content: SectionContent,
1096
+ chart_renderer: ChartRenderer,
1097
+ theme: ThemeConfig,
1098
+ ) -> str:
1099
+ html_parts = [
1100
+ f'<section class="report-section section-patterns" id="section-patterns">',
1101
+ f'<h2>{html.escape(content.title)}</h2>',
1102
+ '<div class="section-content">',
1103
+ ]
1104
+
1105
+ # Pattern charts
1106
+ html_parts.append('<div class="patterns-charts">')
1107
+ html_parts.append(self.render_charts(content.charts, chart_renderer))
1108
+ html_parts.append('</div>')
1109
+
1110
+ # Pattern tables
1111
+ html_parts.append(self.render_tables(content.tables))
1112
+
1113
+ html_parts.extend(['</div>', '</section>'])
1114
+ return '\n'.join(html_parts)
1115
+
1116
+
1117
+ class RecommendationsSectionRenderer(BaseSectionRenderer):
1118
+ """Renderer for recommendations section."""
1119
+
1120
+ def get_section_type(self) -> SectionType:
1121
+ return SectionType.RECOMMENDATIONS
1122
+
1123
+ def render(
1124
+ self,
1125
+ content: SectionContent,
1126
+ chart_renderer: ChartRenderer,
1127
+ theme: ThemeConfig,
1128
+ ) -> str:
1129
+ html_parts = [
1130
+ f'<section class="report-section section-recommendations" id="section-recommendations">',
1131
+ f'<h2>{html.escape(content.title)}</h2>',
1132
+ '<div class="section-content">',
1133
+ ]
1134
+
1135
+ # Recommendation list
1136
+ if content.text_blocks:
1137
+ html_parts.append('<ul class="recommendations-list">')
1138
+ for rec in content.text_blocks:
1139
+ html_parts.append(f'<li>{html.escape(rec)}</li>')
1140
+ html_parts.append('</ul>')
1141
+
1142
+ # Additional info
1143
+ html_parts.append(self.render_tables(content.tables))
1144
+
1145
+ html_parts.extend(['</div>', '</section>'])
1146
+ return '\n'.join(html_parts)
1147
+
1148
+
1149
+ class CustomSectionRenderer(BaseSectionRenderer):
1150
+ """Renderer for custom sections."""
1151
+
1152
+ def get_section_type(self) -> SectionType:
1153
+ return SectionType.CUSTOM
1154
+
1155
+ def render(
1156
+ self,
1157
+ content: SectionContent,
1158
+ chart_renderer: ChartRenderer,
1159
+ theme: ThemeConfig,
1160
+ ) -> str:
1161
+ html_parts = [
1162
+ f'<section class="report-section section-custom" id="section-{content.metadata.get("id", "custom")}">',
1163
+ f'<h2>{html.escape(content.title)}</h2>',
1164
+ '<div class="section-content">',
1165
+ ]
1166
+
1167
+ # Render in order: text, alerts, charts, tables
1168
+ for text in content.text_blocks:
1169
+ html_parts.append(f'<p>{text}</p>')
1170
+
1171
+ html_parts.append(self.render_alerts(content.alerts))
1172
+ html_parts.append(self.render_charts(content.charts, chart_renderer))
1173
+ html_parts.append(self.render_tables(content.tables))
1174
+
1175
+ html_parts.extend(['</div>', '</section>'])
1176
+ return '\n'.join(html_parts)
1177
+
1178
+
1179
+ # =============================================================================
1180
+ # Template Engine
1181
+ # =============================================================================
1182
+
1183
+ class ReportTemplate:
1184
+ """Template engine for HTML report generation."""
1185
+
1186
+ def __init__(self, theme: ThemeConfig):
1187
+ self.theme = theme
1188
+
1189
+ def get_css(self, custom_css: Optional[str] = None) -> str:
1190
+ """Generate CSS styles for the report."""
1191
+ t = self.theme
1192
+ css = f'''
1193
+ :root {{
1194
+ --bg-color: {t.background_color};
1195
+ --text-color: {t.text_color};
1196
+ --primary-color: {t.primary_color};
1197
+ --secondary-color: {t.secondary_color};
1198
+ --accent-color: {t.accent_color};
1199
+ --border-color: {t.border_color};
1200
+ --header-bg: {t.header_bg};
1201
+ --card-bg: {t.card_bg};
1202
+ --shadow: {t.shadow};
1203
+ --border-radius: {t.border_radius};
1204
+ --font-family: {t.font_family};
1205
+ }}
1206
+
1207
+ * {{
1208
+ box-sizing: border-box;
1209
+ margin: 0;
1210
+ padding: 0;
1211
+ }}
1212
+
1213
+ body {{
1214
+ font-family: var(--font-family);
1215
+ background-color: var(--bg-color);
1216
+ color: var(--text-color);
1217
+ line-height: 1.6;
1218
+ padding: 20px;
1219
+ }}
1220
+
1221
+ .report-container {{
1222
+ max-width: 1200px;
1223
+ margin: 0 auto;
1224
+ }}
1225
+
1226
+ .report-header {{
1227
+ background: var(--header-bg);
1228
+ padding: 30px;
1229
+ border-radius: var(--border-radius);
1230
+ margin-bottom: 30px;
1231
+ box-shadow: var(--shadow);
1232
+ }}
1233
+
1234
+ .report-header h1 {{
1235
+ color: var(--primary-color);
1236
+ font-size: 2rem;
1237
+ margin-bottom: 10px;
1238
+ }}
1239
+
1240
+ .report-header .subtitle {{
1241
+ color: var(--text-color);
1242
+ opacity: 0.8;
1243
+ font-size: 1.1rem;
1244
+ }}
1245
+
1246
+ .report-header .timestamp {{
1247
+ color: var(--text-color);
1248
+ opacity: 0.6;
1249
+ font-size: 0.9rem;
1250
+ margin-top: 10px;
1251
+ }}
1252
+
1253
+ .report-logo {{
1254
+ max-height: 60px;
1255
+ margin-bottom: 15px;
1256
+ }}
1257
+
1258
+ .toc {{
1259
+ background: var(--card-bg);
1260
+ padding: 20px;
1261
+ border-radius: var(--border-radius);
1262
+ margin-bottom: 30px;
1263
+ box-shadow: var(--shadow);
1264
+ }}
1265
+
1266
+ .toc h3 {{
1267
+ margin-bottom: 15px;
1268
+ color: var(--primary-color);
1269
+ }}
1270
+
1271
+ .toc ul {{
1272
+ list-style: none;
1273
+ }}
1274
+
1275
+ .toc li {{
1276
+ padding: 5px 0;
1277
+ }}
1278
+
1279
+ .toc a {{
1280
+ color: var(--secondary-color);
1281
+ text-decoration: none;
1282
+ }}
1283
+
1284
+ .toc a:hover {{
1285
+ text-decoration: underline;
1286
+ }}
1287
+
1288
+ .report-section {{
1289
+ background: var(--card-bg);
1290
+ padding: 25px;
1291
+ border-radius: var(--border-radius);
1292
+ margin-bottom: 25px;
1293
+ box-shadow: var(--shadow);
1294
+ }}
1295
+
1296
+ .report-section h2 {{
1297
+ color: var(--primary-color);
1298
+ border-bottom: 2px solid var(--border-color);
1299
+ padding-bottom: 10px;
1300
+ margin-bottom: 20px;
1301
+ }}
1302
+
1303
+ .charts-grid {{
1304
+ display: grid;
1305
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1306
+ gap: 20px;
1307
+ margin: 20px 0;
1308
+ }}
1309
+
1310
+ .chart-container {{
1311
+ background: var(--bg-color);
1312
+ padding: 15px;
1313
+ border-radius: var(--border-radius);
1314
+ border: 1px solid var(--border-color);
1315
+ }}
1316
+
1317
+ .column-card {{
1318
+ background: var(--bg-color);
1319
+ padding: 20px;
1320
+ border-radius: var(--border-radius);
1321
+ border: 1px solid var(--border-color);
1322
+ margin-bottom: 15px;
1323
+ }}
1324
+
1325
+ .column-card h3 {{
1326
+ color: var(--secondary-color);
1327
+ margin-bottom: 15px;
1328
+ font-size: 1.1rem;
1329
+ }}
1330
+
1331
+ .data-table {{
1332
+ width: 100%;
1333
+ border-collapse: collapse;
1334
+ margin: 15px 0;
1335
+ }}
1336
+
1337
+ .data-table th,
1338
+ .data-table td {{
1339
+ padding: 12px;
1340
+ text-align: left;
1341
+ border-bottom: 1px solid var(--border-color);
1342
+ }}
1343
+
1344
+ .data-table th {{
1345
+ background: var(--header-bg);
1346
+ font-weight: 600;
1347
+ color: var(--primary-color);
1348
+ }}
1349
+
1350
+ .data-table tr:hover {{
1351
+ background: var(--header-bg);
1352
+ }}
1353
+
1354
+ .alert {{
1355
+ padding: 15px;
1356
+ border-radius: var(--border-radius);
1357
+ margin: 10px 0;
1358
+ }}
1359
+
1360
+ .alert-info {{
1361
+ background: #e3f2fd;
1362
+ border-left: 4px solid #2196f3;
1363
+ color: #1565c0;
1364
+ }}
1365
+
1366
+ .alert-warning {{
1367
+ background: #fff3e0;
1368
+ border-left: 4px solid #ff9800;
1369
+ color: #e65100;
1370
+ }}
1371
+
1372
+ .alert-error {{
1373
+ background: #ffebee;
1374
+ border-left: 4px solid #f44336;
1375
+ color: #c62828;
1376
+ }}
1377
+
1378
+ .alert-success {{
1379
+ background: #e8f5e9;
1380
+ border-left: 4px solid #4caf50;
1381
+ color: #2e7d32;
1382
+ }}
1383
+
1384
+ .quality-gauges {{
1385
+ display: flex;
1386
+ flex-wrap: wrap;
1387
+ gap: 20px;
1388
+ justify-content: center;
1389
+ margin: 20px 0;
1390
+ }}
1391
+
1392
+ .recommendations-list {{
1393
+ list-style: disc;
1394
+ padding-left: 25px;
1395
+ }}
1396
+
1397
+ .recommendations-list li {{
1398
+ padding: 8px 0;
1399
+ }}
1400
+
1401
+ .report-footer {{
1402
+ text-align: center;
1403
+ padding: 20px;
1404
+ color: var(--text-color);
1405
+ opacity: 0.6;
1406
+ font-size: 0.9rem;
1407
+ }}
1408
+
1409
+ @media print {{
1410
+ body {{
1411
+ padding: 0;
1412
+ }}
1413
+ .report-section {{
1414
+ break-inside: avoid;
1415
+ box-shadow: none;
1416
+ border: 1px solid var(--border-color);
1417
+ }}
1418
+ }}
1419
+ '''
1420
+
1421
+ if custom_css:
1422
+ css += f'\n/* Custom CSS */\n{custom_css}'
1423
+
1424
+ return css
1425
+
1426
+ def get_js(self, custom_js: Optional[str] = None) -> str:
1427
+ """Generate JavaScript for the report."""
1428
+ js = '''
1429
+ document.addEventListener('DOMContentLoaded', function() {
1430
+ // Smooth scroll for TOC links
1431
+ document.querySelectorAll('.toc a').forEach(function(link) {
1432
+ link.addEventListener('click', function(e) {
1433
+ e.preventDefault();
1434
+ var targetId = this.getAttribute('href').substring(1);
1435
+ var target = document.getElementById(targetId);
1436
+ if (target) {
1437
+ target.scrollIntoView({ behavior: 'smooth' });
1438
+ }
1439
+ });
1440
+ });
1441
+
1442
+ // Collapsible sections
1443
+ document.querySelectorAll('.collapsible h2').forEach(function(header) {
1444
+ header.style.cursor = 'pointer';
1445
+ header.addEventListener('click', function() {
1446
+ var content = this.nextElementSibling;
1447
+ if (content.style.display === 'none') {
1448
+ content.style.display = 'block';
1449
+ this.classList.remove('collapsed');
1450
+ } else {
1451
+ content.style.display = 'none';
1452
+ this.classList.add('collapsed');
1453
+ }
1454
+ });
1455
+ });
1456
+ });
1457
+ '''
1458
+
1459
+ if custom_js:
1460
+ js += f'\n// Custom JS\n{custom_js}'
1461
+
1462
+ return js
1463
+
1464
+ def render_header(self, config: ReportConfig) -> str:
1465
+ """Render report header."""
1466
+ logo_html = ""
1467
+ if config.logo_base64:
1468
+ logo_html = f'<img src="data:image/png;base64,{config.logo_base64}" class="report-logo" alt="Logo">'
1469
+ elif config.logo_path and os.path.exists(config.logo_path):
1470
+ with open(config.logo_path, "rb") as f:
1471
+ b64 = base64.b64encode(f.read()).decode()
1472
+ ext = Path(config.logo_path).suffix.lower()
1473
+ mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "svg": "image/svg+xml"}.get(ext[1:], "image/png")
1474
+ logo_html = f'<img src="data:{mime};base64,{b64}" class="report-logo" alt="Logo">'
1475
+
1476
+ timestamp_html = ""
1477
+ if config.include_timestamp:
1478
+ timestamp_html = f'<div class="timestamp">Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</div>'
1479
+
1480
+ subtitle_html = ""
1481
+ if config.subtitle:
1482
+ subtitle_html = f'<div class="subtitle">{html.escape(config.subtitle)}</div>'
1483
+
1484
+ return f'''
1485
+ <header class="report-header">
1486
+ {logo_html}
1487
+ <h1>{html.escape(config.title)}</h1>
1488
+ {subtitle_html}
1489
+ {timestamp_html}
1490
+ </header>
1491
+ '''
1492
+
1493
+ def render_toc(self, sections: List[SectionContent]) -> str:
1494
+ """Render table of contents."""
1495
+ toc_items = []
1496
+ for section in sections:
1497
+ section_id = f"section-{section.section_type.value}"
1498
+ toc_items.append(f'<li><a href="#{section_id}">{html.escape(section.title)}</a></li>')
1499
+
1500
+ return f'''
1501
+ <nav class="toc">
1502
+ <h3>Table of Contents</h3>
1503
+ <ul>
1504
+ {''.join(toc_items)}
1505
+ </ul>
1506
+ </nav>
1507
+ '''
1508
+
1509
+ def render_footer(self) -> str:
1510
+ """Render report footer."""
1511
+ return '''
1512
+ <footer class="report-footer">
1513
+ <p>Generated by Truthound Data Profiler</p>
1514
+ </footer>
1515
+ '''
1516
+
1517
+ def render_document(
1518
+ self,
1519
+ config: ReportConfig,
1520
+ sections_html: str,
1521
+ toc_html: str = "",
1522
+ ) -> str:
1523
+ """Render complete HTML document."""
1524
+ css = self.get_css(config.custom_css)
1525
+ js = self.get_js(config.custom_js)
1526
+ header = self.render_header(config)
1527
+ footer = self.render_footer()
1528
+
1529
+ return f'''<!DOCTYPE html>
1530
+ <html lang="{config.language}">
1531
+ <head>
1532
+ <meta charset="UTF-8">
1533
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1534
+ <title>{html.escape(config.title)}</title>
1535
+ <style>
1536
+ {css}
1537
+ </style>
1538
+ </head>
1539
+ <body>
1540
+ <div class="report-container">
1541
+ {header}
1542
+ {toc_html}
1543
+ <main class="report-content">
1544
+ {sections_html}
1545
+ </main>
1546
+ {footer}
1547
+ </div>
1548
+ <script>
1549
+ {js}
1550
+ </script>
1551
+ </body>
1552
+ </html>'''
1553
+
1554
+
1555
+ # =============================================================================
1556
+ # Profile Data Converter
1557
+ # =============================================================================
1558
+
1559
+ class ProfileDataConverter:
1560
+ """Converts ProfileData to SectionContent for rendering."""
1561
+
1562
+ def __init__(self, profile: ProfileData):
1563
+ self.profile = profile
1564
+
1565
+ def create_overview_section(self) -> SectionContent:
1566
+ """Create overview section from profile data."""
1567
+ # Summary text
1568
+ text_blocks = [
1569
+ f"Table: <strong>{html.escape(self.profile.table_name)}</strong>",
1570
+ f"Rows: <strong>{self.profile.row_count:,}</strong> | "
1571
+ f"Columns: <strong>{self.profile.column_count}</strong>",
1572
+ ]
1573
+
1574
+ if self.profile.timestamp:
1575
+ text_blocks.append(f"Profiled at: {self.profile.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
1576
+
1577
+ # Data type distribution chart
1578
+ type_counts: Dict[str, int] = {}
1579
+ for col in self.profile.columns:
1580
+ dtype = col.get("inferred_type", col.get("data_type", "unknown"))
1581
+ type_counts[dtype] = type_counts.get(dtype, 0) + 1
1582
+
1583
+ charts = []
1584
+ if type_counts:
1585
+ charts.append((
1586
+ ChartData(
1587
+ labels=list(type_counts.keys()),
1588
+ values=list(type_counts.values()),
1589
+ title="Data Type Distribution",
1590
+ show_legend=True,
1591
+ ),
1592
+ ChartConfig(chart_type=ChartType.DONUT, width=350, height=300)
1593
+ ))
1594
+
1595
+ # Summary statistics table
1596
+ tables = []
1597
+ if self.profile.columns:
1598
+ tables.append({
1599
+ "headers": ["Metric", "Value"],
1600
+ "rows": [
1601
+ ["Total Columns", self.profile.column_count],
1602
+ ["Total Rows", f"{self.profile.row_count:,}"],
1603
+ ["Numeric Columns", sum(1 for c in self.profile.columns if c.get("is_numeric", False))],
1604
+ ["Text Columns", sum(1 for c in self.profile.columns if c.get("data_type") == "string")],
1605
+ ["Columns with Nulls", sum(1 for c in self.profile.columns if c.get("null_count", 0) > 0)],
1606
+ ]
1607
+ })
1608
+
1609
+ return SectionContent(
1610
+ section_type=SectionType.OVERVIEW,
1611
+ title="Overview",
1612
+ text_blocks=text_blocks,
1613
+ charts=charts,
1614
+ tables=tables,
1615
+ priority=100,
1616
+ )
1617
+
1618
+ def create_quality_section(self) -> SectionContent:
1619
+ """Create data quality section from profile data."""
1620
+ charts = []
1621
+ alerts = []
1622
+ tables = []
1623
+
1624
+ # Quality score gauge
1625
+ if self.profile.quality_scores:
1626
+ overall = self.profile.quality_scores.get("overall", 0)
1627
+ charts.append((
1628
+ ChartData(
1629
+ values=[overall * 100],
1630
+ title="Overall Quality Score",
1631
+ metadata={"min": 0, "max": 100},
1632
+ ),
1633
+ ChartConfig(chart_type=ChartType.GAUGE, width=250, height=180)
1634
+ ))
1635
+
1636
+ # Completeness chart
1637
+ completeness_data = []
1638
+ for col in self.profile.columns[:10]: # Top 10
1639
+ null_pct = (col.get("null_count", 0) / self.profile.row_count * 100) if self.profile.row_count > 0 else 0
1640
+ completeness_data.append({
1641
+ "name": col.get("name", "unknown"),
1642
+ "completeness": 100 - null_pct,
1643
+ })
1644
+
1645
+ if completeness_data:
1646
+ charts.append((
1647
+ ChartData(
1648
+ labels=[d["name"] for d in completeness_data],
1649
+ values=[d["completeness"] for d in completeness_data],
1650
+ title="Column Completeness (%)",
1651
+ ),
1652
+ ChartConfig(chart_type=ChartType.HORIZONTAL_BAR, width=400, height=300)
1653
+ ))
1654
+
1655
+ # Alerts from profile
1656
+ if self.profile.alerts:
1657
+ for alert in self.profile.alerts:
1658
+ alerts.append({
1659
+ "level": alert.get("severity", "info"),
1660
+ "message": alert.get("message", str(alert)),
1661
+ })
1662
+
1663
+ # Quality metrics table
1664
+ quality_rows = []
1665
+ for col in self.profile.columns:
1666
+ null_pct = (col.get("null_count", 0) / self.profile.row_count * 100) if self.profile.row_count > 0 else 0
1667
+ unique_pct = (col.get("unique_count", 0) / self.profile.row_count * 100) if self.profile.row_count > 0 else 0
1668
+ quality_rows.append([
1669
+ col.get("name", "unknown"),
1670
+ f"{100 - null_pct:.1f}%",
1671
+ f"{unique_pct:.1f}%",
1672
+ col.get("inferred_type", "unknown"),
1673
+ ])
1674
+
1675
+ if quality_rows:
1676
+ tables.append({
1677
+ "headers": ["Column", "Completeness", "Uniqueness", "Type"],
1678
+ "rows": quality_rows,
1679
+ })
1680
+
1681
+ return SectionContent(
1682
+ section_type=SectionType.DATA_QUALITY,
1683
+ title="Data Quality",
1684
+ charts=charts,
1685
+ alerts=alerts,
1686
+ tables=tables,
1687
+ priority=90,
1688
+ )
1689
+
1690
+ def create_columns_section(self) -> SectionContent:
1691
+ """Create column details section from profile data."""
1692
+ charts = []
1693
+
1694
+ for col in self.profile.columns:
1695
+ col_name = col.get("name", "unknown")
1696
+
1697
+ # Value distribution if available
1698
+ value_counts = col.get("value_counts", {})
1699
+ if value_counts and len(value_counts) <= 20:
1700
+ sorted_counts = sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[:10]
1701
+ charts.append((
1702
+ ChartData(
1703
+ labels=[str(k) for k, v in sorted_counts],
1704
+ values=[v for k, v in sorted_counts],
1705
+ title=col_name,
1706
+ ),
1707
+ ChartConfig(chart_type=ChartType.BAR, width=350, height=250)
1708
+ ))
1709
+ elif col.get("histogram"):
1710
+ # Histogram data
1711
+ hist = col["histogram"]
1712
+ charts.append((
1713
+ ChartData(
1714
+ labels=hist.get("bins", []),
1715
+ values=hist.get("counts", []),
1716
+ title=col_name,
1717
+ ),
1718
+ ChartConfig(chart_type=ChartType.HISTOGRAM, width=350, height=250)
1719
+ ))
1720
+
1721
+ # Column statistics table
1722
+ tables = []
1723
+ col_rows = []
1724
+ for col in self.profile.columns:
1725
+ stats = col.get("statistics", {})
1726
+ col_rows.append([
1727
+ col.get("name", "unknown"),
1728
+ col.get("data_type", "unknown"),
1729
+ col.get("unique_count", "N/A"),
1730
+ col.get("null_count", 0),
1731
+ stats.get("mean", "N/A") if stats else "N/A",
1732
+ stats.get("min", "N/A") if stats else "N/A",
1733
+ stats.get("max", "N/A") if stats else "N/A",
1734
+ ])
1735
+
1736
+ if col_rows:
1737
+ tables.append({
1738
+ "headers": ["Name", "Type", "Unique", "Nulls", "Mean", "Min", "Max"],
1739
+ "rows": col_rows,
1740
+ })
1741
+
1742
+ return SectionContent(
1743
+ section_type=SectionType.COLUMN_DETAILS,
1744
+ title="Column Details",
1745
+ charts=charts,
1746
+ tables=tables,
1747
+ collapsible=True,
1748
+ priority=70,
1749
+ )
1750
+
1751
+ def create_patterns_section(self) -> SectionContent:
1752
+ """Create patterns section from profile data."""
1753
+ charts = []
1754
+ tables = []
1755
+
1756
+ if self.profile.patterns_found:
1757
+ # Pattern distribution
1758
+ pattern_types: Dict[str, int] = {}
1759
+ for p in self.profile.patterns_found:
1760
+ ptype = p.get("pattern_type", "unknown")
1761
+ pattern_types[ptype] = pattern_types.get(ptype, 0) + 1
1762
+
1763
+ if pattern_types:
1764
+ charts.append((
1765
+ ChartData(
1766
+ labels=list(pattern_types.keys()),
1767
+ values=list(pattern_types.values()),
1768
+ title="Pattern Types Found",
1769
+ ),
1770
+ ChartConfig(chart_type=ChartType.PIE, width=350, height=300)
1771
+ ))
1772
+
1773
+ # Pattern details table
1774
+ pattern_rows = []
1775
+ for p in self.profile.patterns_found[:20]: # Limit
1776
+ pattern_rows.append([
1777
+ p.get("column", "N/A"),
1778
+ p.get("pattern_type", "unknown"),
1779
+ p.get("pattern", "N/A"),
1780
+ f"{p.get('confidence', 0) * 100:.1f}%",
1781
+ p.get("sample_count", "N/A"),
1782
+ ])
1783
+
1784
+ if pattern_rows:
1785
+ tables.append({
1786
+ "headers": ["Column", "Type", "Pattern", "Confidence", "Samples"],
1787
+ "rows": pattern_rows,
1788
+ })
1789
+
1790
+ return SectionContent(
1791
+ section_type=SectionType.PATTERNS,
1792
+ title="Detected Patterns",
1793
+ charts=charts,
1794
+ tables=tables,
1795
+ priority=60,
1796
+ )
1797
+
1798
+ def create_recommendations_section(self) -> SectionContent:
1799
+ """Create recommendations section from profile data."""
1800
+ text_blocks = []
1801
+
1802
+ if self.profile.recommendations:
1803
+ text_blocks = self.profile.recommendations
1804
+ else:
1805
+ # Generate basic recommendations
1806
+ for col in self.profile.columns:
1807
+ null_pct = (col.get("null_count", 0) / self.profile.row_count * 100) if self.profile.row_count > 0 else 0
1808
+ if null_pct > 20:
1809
+ text_blocks.append(
1810
+ f"Column '{col.get('name')}' has {null_pct:.1f}% null values. "
1811
+ f"Consider handling missing data."
1812
+ )
1813
+
1814
+ unique_pct = (col.get("unique_count", 0) / self.profile.row_count * 100) if self.profile.row_count > 0 else 0
1815
+ if unique_pct == 100 and self.profile.row_count > 1:
1816
+ text_blocks.append(
1817
+ f"Column '{col.get('name')}' appears to be a unique identifier."
1818
+ )
1819
+
1820
+ if not text_blocks:
1821
+ text_blocks = ["No specific recommendations. Data quality appears good."]
1822
+
1823
+ return SectionContent(
1824
+ section_type=SectionType.RECOMMENDATIONS,
1825
+ title="Recommendations",
1826
+ text_blocks=text_blocks,
1827
+ priority=50,
1828
+ )
1829
+
1830
+
1831
+ # =============================================================================
1832
+ # Main Report Generator
1833
+ # =============================================================================
1834
+
1835
+ class HTMLReportGenerator:
1836
+ """Main interface for generating HTML reports from profile data."""
1837
+
1838
+ def __init__(
1839
+ self,
1840
+ chart_renderer: Optional[ChartRenderer] = None,
1841
+ theme: Optional[ThemeConfig] = None,
1842
+ ):
1843
+ self.chart_renderer = chart_renderer or SVGChartRenderer()
1844
+ self._theme = theme
1845
+ self._section_renderers: Dict[SectionType, SectionRenderer] = {}
1846
+ self._register_default_renderers()
1847
+
1848
+ def _register_default_renderers(self) -> None:
1849
+ """Register default section renderers."""
1850
+ self._section_renderers[SectionType.OVERVIEW] = OverviewSectionRenderer()
1851
+ self._section_renderers[SectionType.DATA_QUALITY] = DataQualitySectionRenderer()
1852
+ self._section_renderers[SectionType.COLUMN_DETAILS] = ColumnDetailsSectionRenderer()
1853
+ self._section_renderers[SectionType.PATTERNS] = PatternsSectionRenderer()
1854
+ self._section_renderers[SectionType.RECOMMENDATIONS] = RecommendationsSectionRenderer()
1855
+ self._section_renderers[SectionType.CUSTOM] = CustomSectionRenderer()
1856
+
1857
+ def register_section_renderer(self, renderer: SectionRenderer) -> None:
1858
+ """Register a custom section renderer."""
1859
+ self._section_renderers[renderer.get_section_type()] = renderer
1860
+
1861
+ def generate(
1862
+ self,
1863
+ profile: ProfileData,
1864
+ config: Optional[ReportConfig] = None,
1865
+ ) -> str:
1866
+ """Generate HTML report from profile data."""
1867
+ config = config or ReportConfig()
1868
+ theme = self._theme or theme_registry.get(config.theme)
1869
+ template = ReportTemplate(theme)
1870
+
1871
+ # Convert profile to sections
1872
+ converter = ProfileDataConverter(profile)
1873
+ sections: List[SectionContent] = []
1874
+
1875
+ for section_type in config.sections:
1876
+ if section_type == SectionType.OVERVIEW:
1877
+ sections.append(converter.create_overview_section())
1878
+ elif section_type == SectionType.DATA_QUALITY:
1879
+ sections.append(converter.create_quality_section())
1880
+ elif section_type == SectionType.COLUMN_DETAILS:
1881
+ sections.append(converter.create_columns_section())
1882
+ elif section_type == SectionType.PATTERNS:
1883
+ sections.append(converter.create_patterns_section())
1884
+ elif section_type == SectionType.RECOMMENDATIONS:
1885
+ sections.append(converter.create_recommendations_section())
1886
+
1887
+ # Sort by priority
1888
+ sections.sort(key=lambda s: s.priority, reverse=True)
1889
+
1890
+ # Render sections
1891
+ sections_html = []
1892
+ for section in sections:
1893
+ renderer = self._section_renderers.get(section.section_type)
1894
+ if renderer:
1895
+ sections_html.append(renderer.render(section, self.chart_renderer, theme))
1896
+
1897
+ # Generate TOC if requested
1898
+ toc_html = template.render_toc(sections) if config.include_toc else ""
1899
+
1900
+ # Render document
1901
+ return template.render_document(config, '\n'.join(sections_html), toc_html)
1902
+
1903
+ def generate_from_dict(
1904
+ self,
1905
+ profile_dict: Dict[str, Any],
1906
+ config: Optional[ReportConfig] = None,
1907
+ ) -> str:
1908
+ """Generate HTML report from profile dictionary."""
1909
+ profile = ProfileData(
1910
+ table_name=profile_dict.get("table_name", "Unknown"),
1911
+ row_count=profile_dict.get("row_count", 0),
1912
+ column_count=profile_dict.get("column_count", len(profile_dict.get("columns", []))),
1913
+ columns=profile_dict.get("columns", []),
1914
+ quality_scores=profile_dict.get("quality_scores"),
1915
+ patterns_found=profile_dict.get("patterns"),
1916
+ alerts=profile_dict.get("alerts"),
1917
+ recommendations=profile_dict.get("recommendations"),
1918
+ timestamp=datetime.fromisoformat(profile_dict["timestamp"]) if profile_dict.get("timestamp") else None,
1919
+ metadata=profile_dict.get("metadata", {}),
1920
+ )
1921
+ return self.generate(profile, config)
1922
+
1923
+ def save(
1924
+ self,
1925
+ profile: ProfileData,
1926
+ output_path: Union[str, Path],
1927
+ config: Optional[ReportConfig] = None,
1928
+ ) -> Path:
1929
+ """Generate and save HTML report to file."""
1930
+ html_content = self.generate(profile, config)
1931
+ output_path = Path(output_path)
1932
+ output_path.write_text(html_content, encoding="utf-8")
1933
+ return output_path
1934
+
1935
+
1936
+ # =============================================================================
1937
+ # Report Exporter
1938
+ # =============================================================================
1939
+
1940
+ class ReportExporter:
1941
+ """Export reports to various formats."""
1942
+
1943
+ def __init__(self, generator: Optional[HTMLReportGenerator] = None):
1944
+ self.generator = generator or HTMLReportGenerator()
1945
+
1946
+ def to_html(
1947
+ self,
1948
+ profile: ProfileData,
1949
+ config: Optional[ReportConfig] = None,
1950
+ ) -> str:
1951
+ """Export to HTML string."""
1952
+ return self.generator.generate(profile, config)
1953
+
1954
+ def to_json(self, profile: ProfileData) -> str:
1955
+ """Export profile data to JSON."""
1956
+ data = {
1957
+ "table_name": profile.table_name,
1958
+ "row_count": profile.row_count,
1959
+ "column_count": profile.column_count,
1960
+ "columns": profile.columns,
1961
+ "quality_scores": profile.quality_scores,
1962
+ "patterns_found": profile.patterns_found,
1963
+ "alerts": profile.alerts,
1964
+ "recommendations": profile.recommendations,
1965
+ "timestamp": profile.timestamp.isoformat() if profile.timestamp else None,
1966
+ "metadata": profile.metadata,
1967
+ }
1968
+ return json.dumps(data, indent=2, default=str)
1969
+
1970
+ def to_file(
1971
+ self,
1972
+ profile: ProfileData,
1973
+ output_path: Union[str, Path],
1974
+ format: str = "html",
1975
+ config: Optional[ReportConfig] = None,
1976
+ ) -> Path:
1977
+ """Export to file with specified format."""
1978
+ output_path = Path(output_path)
1979
+
1980
+ if format.lower() == "html":
1981
+ content = self.to_html(profile, config)
1982
+ output_path.write_text(content, encoding="utf-8")
1983
+ elif format.lower() == "json":
1984
+ content = self.to_json(profile)
1985
+ output_path.write_text(content, encoding="utf-8")
1986
+ else:
1987
+ raise ValueError(f"Unsupported format: {format}")
1988
+
1989
+ return output_path
1990
+
1991
+
1992
+ # =============================================================================
1993
+ # Convenience Functions
1994
+ # =============================================================================
1995
+
1996
+ def generate_report(
1997
+ profile: Union[ProfileData, Dict[str, Any]],
1998
+ output_path: Optional[Union[str, Path]] = None,
1999
+ config: Optional[ReportConfig] = None,
2000
+ theme: Optional[ReportTheme] = None,
2001
+ ) -> Union[str, Path]:
2002
+ """
2003
+ Convenience function to generate an HTML report.
2004
+
2005
+ Args:
2006
+ profile: Profile data or dictionary
2007
+ output_path: Optional path to save report
2008
+ config: Report configuration
2009
+ theme: Report theme
2010
+
2011
+ Returns:
2012
+ HTML string if no output_path, Path if saved to file
2013
+ """
2014
+ if config is None:
2015
+ config = ReportConfig()
2016
+
2017
+ if theme:
2018
+ config.theme = theme
2019
+
2020
+ generator = HTMLReportGenerator()
2021
+
2022
+ if isinstance(profile, dict):
2023
+ html_content = generator.generate_from_dict(profile, config)
2024
+ else:
2025
+ html_content = generator.generate(profile, config)
2026
+
2027
+ if output_path:
2028
+ path = Path(output_path)
2029
+ path.write_text(html_content, encoding="utf-8")
2030
+ return path
2031
+
2032
+ return html_content
2033
+
2034
+
2035
+ def compare_profiles(
2036
+ profiles: List[ProfileData],
2037
+ labels: Optional[List[str]] = None,
2038
+ config: Optional[ReportConfig] = None,
2039
+ ) -> str:
2040
+ """
2041
+ Generate a comparison report for multiple profiles.
2042
+
2043
+ Args:
2044
+ profiles: List of profile data to compare
2045
+ labels: Optional labels for each profile
2046
+ config: Report configuration
2047
+
2048
+ Returns:
2049
+ HTML report string
2050
+ """
2051
+ config = config or ReportConfig(title="Profile Comparison Report")
2052
+ labels = labels or [f"Profile {i+1}" for i in range(len(profiles))]
2053
+
2054
+ # Create comparison sections
2055
+ sections: List[SectionContent] = []
2056
+
2057
+ # Overview comparison
2058
+ overview_rows = []
2059
+ for label, profile in zip(labels, profiles):
2060
+ overview_rows.append([
2061
+ label,
2062
+ profile.table_name,
2063
+ f"{profile.row_count:,}",
2064
+ profile.column_count,
2065
+ ])
2066
+
2067
+ sections.append(SectionContent(
2068
+ section_type=SectionType.OVERVIEW,
2069
+ title="Profile Comparison",
2070
+ tables=[{
2071
+ "headers": ["Label", "Table", "Rows", "Columns"],
2072
+ "rows": overview_rows,
2073
+ }],
2074
+ priority=100,
2075
+ ))
2076
+
2077
+ # Quality comparison chart
2078
+ if all(p.quality_scores for p in profiles):
2079
+ quality_data = ChartData(
2080
+ labels=labels,
2081
+ values=[p.quality_scores.get("overall", 0) * 100 for p in profiles],
2082
+ title="Quality Score Comparison",
2083
+ )
2084
+ sections.append(SectionContent(
2085
+ section_type=SectionType.DATA_QUALITY,
2086
+ title="Quality Comparison",
2087
+ charts=[(quality_data, ChartConfig(chart_type=ChartType.BAR, width=500, height=300))],
2088
+ priority=90,
2089
+ ))
2090
+
2091
+ # Generate report
2092
+ theme = theme_registry.get(config.theme)
2093
+ template = ReportTemplate(theme)
2094
+ generator = HTMLReportGenerator()
2095
+
2096
+ sections_html = []
2097
+ for section in sections:
2098
+ renderer = generator._section_renderers.get(section.section_type)
2099
+ if renderer:
2100
+ sections_html.append(renderer.render(section, generator.chart_renderer, theme))
2101
+
2102
+ return template.render_document(
2103
+ config,
2104
+ '\n'.join(sections_html),
2105
+ template.render_toc(sections) if config.include_toc else "",
2106
+ )
2107
+
2108
+
2109
+ # =============================================================================
2110
+ # Register Default Renderers
2111
+ # =============================================================================
2112
+
2113
+ # Register SVG renderer as default
2114
+ chart_renderer_registry.register("svg", SVGChartRenderer(), default=True)
2115
+
2116
+ # Register section renderers
2117
+ section_registry.register(OverviewSectionRenderer())
2118
+ section_registry.register(DataQualitySectionRenderer())
2119
+ section_registry.register(ColumnDetailsSectionRenderer())
2120
+ section_registry.register(PatternsSectionRenderer())
2121
+ section_registry.register(RecommendationsSectionRenderer())
2122
+ section_registry.register(CustomSectionRenderer())