truthound 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
"""Enterprise-grade connection pool management for database stores.
|
|
2
|
+
|
|
3
|
+
This module provides a highly extensible and maintainable connection pool
|
|
4
|
+
abstraction layer with:
|
|
5
|
+
- Multiple pooling strategies (QueuePool, NullPool, StaticPool, AsyncAdaptedQueuePool)
|
|
6
|
+
- Connection health monitoring and automatic recovery
|
|
7
|
+
- Circuit breaker pattern for fault tolerance
|
|
8
|
+
- Comprehensive metrics and observability
|
|
9
|
+
- Database-specific optimizations
|
|
10
|
+
- Retry logic with exponential backoff
|
|
11
|
+
|
|
12
|
+
Install with: pip install truthound[database]
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
import weakref
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from collections import deque
|
|
23
|
+
from contextlib import contextmanager
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import datetime, timedelta, timezone
|
|
26
|
+
from enum import Enum, auto
|
|
27
|
+
from typing import (
|
|
28
|
+
TYPE_CHECKING,
|
|
29
|
+
Any,
|
|
30
|
+
Callable,
|
|
31
|
+
Generic,
|
|
32
|
+
Iterator,
|
|
33
|
+
Protocol,
|
|
34
|
+
TypeVar,
|
|
35
|
+
runtime_checkable,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
# Lazy import SQLAlchemy
|
|
41
|
+
try:
|
|
42
|
+
from sqlalchemy import create_engine, event, text
|
|
43
|
+
from sqlalchemy.engine import Engine
|
|
44
|
+
from sqlalchemy.exc import (
|
|
45
|
+
DBAPIError,
|
|
46
|
+
DisconnectionError,
|
|
47
|
+
InterfaceError,
|
|
48
|
+
OperationalError,
|
|
49
|
+
SQLAlchemyError,
|
|
50
|
+
)
|
|
51
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
52
|
+
from sqlalchemy.pool import (
|
|
53
|
+
NullPool,
|
|
54
|
+
QueuePool,
|
|
55
|
+
SingletonThreadPool,
|
|
56
|
+
StaticPool,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
HAS_SQLALCHEMY = True
|
|
60
|
+
except ImportError:
|
|
61
|
+
HAS_SQLALCHEMY = False
|
|
62
|
+
SQLAlchemyError = Exception # type: ignore
|
|
63
|
+
DBAPIError = Exception # type: ignore
|
|
64
|
+
OperationalError = Exception # type: ignore
|
|
65
|
+
DisconnectionError = Exception # type: ignore
|
|
66
|
+
InterfaceError = Exception # type: ignore
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from sqlalchemy.engine import Engine
|
|
70
|
+
from sqlalchemy.orm import Session
|
|
71
|
+
from sqlalchemy.pool import Pool
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# =============================================================================
|
|
75
|
+
# Enums and Constants
|
|
76
|
+
# =============================================================================
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PoolStrategy(Enum):
|
|
80
|
+
"""Connection pool strategy types."""
|
|
81
|
+
|
|
82
|
+
QUEUE_POOL = auto() # Standard pool with overflow
|
|
83
|
+
NULL_POOL = auto() # No pooling, new connection each time
|
|
84
|
+
STATIC_POOL = auto() # Single connection for all requests
|
|
85
|
+
SINGLETON_THREAD = auto() # One connection per thread
|
|
86
|
+
ASYNC_QUEUE = auto() # Async-compatible queue pool
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DatabaseDialect(Enum):
|
|
90
|
+
"""Supported database dialects with specific optimizations."""
|
|
91
|
+
|
|
92
|
+
POSTGRESQL = "postgresql"
|
|
93
|
+
MYSQL = "mysql"
|
|
94
|
+
SQLITE = "sqlite"
|
|
95
|
+
MSSQL = "mssql"
|
|
96
|
+
ORACLE = "oracle"
|
|
97
|
+
UNKNOWN = "unknown"
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_url(cls, url: str) -> "DatabaseDialect":
|
|
101
|
+
"""Detect dialect from connection URL."""
|
|
102
|
+
url_lower = url.lower()
|
|
103
|
+
if url_lower.startswith("postgresql") or url_lower.startswith("postgres"):
|
|
104
|
+
return cls.POSTGRESQL
|
|
105
|
+
elif url_lower.startswith("mysql"):
|
|
106
|
+
return cls.MYSQL
|
|
107
|
+
elif url_lower.startswith("sqlite"):
|
|
108
|
+
return cls.SQLITE
|
|
109
|
+
elif url_lower.startswith("mssql") or "pyodbc" in url_lower:
|
|
110
|
+
return cls.MSSQL
|
|
111
|
+
elif url_lower.startswith("oracle"):
|
|
112
|
+
return cls.ORACLE
|
|
113
|
+
return cls.UNKNOWN
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ConnectionState(Enum):
|
|
117
|
+
"""State of a pooled connection."""
|
|
118
|
+
|
|
119
|
+
IDLE = auto()
|
|
120
|
+
IN_USE = auto()
|
|
121
|
+
STALE = auto()
|
|
122
|
+
BROKEN = auto()
|
|
123
|
+
RECYCLING = auto()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class CircuitState(Enum):
|
|
127
|
+
"""Circuit breaker states."""
|
|
128
|
+
|
|
129
|
+
CLOSED = auto() # Normal operation
|
|
130
|
+
OPEN = auto() # Failing, reject requests
|
|
131
|
+
HALF_OPEN = auto() # Testing if recovered
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# Configuration Dataclasses
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class PoolConfig:
|
|
141
|
+
"""Connection pool configuration.
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
strategy: Pooling strategy to use.
|
|
145
|
+
pool_size: Number of connections to maintain in pool.
|
|
146
|
+
max_overflow: Maximum overflow connections beyond pool_size.
|
|
147
|
+
pool_timeout: Seconds to wait for available connection.
|
|
148
|
+
pool_recycle: Seconds before a connection is recycled (-1 = never).
|
|
149
|
+
pool_pre_ping: Whether to test connections before use.
|
|
150
|
+
echo_pool: Log pool checkouts/checkins for debugging.
|
|
151
|
+
reset_on_return: How to reset connections when returned.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
strategy: PoolStrategy = PoolStrategy.QUEUE_POOL
|
|
155
|
+
pool_size: int = 5
|
|
156
|
+
max_overflow: int = 10
|
|
157
|
+
pool_timeout: float = 30.0
|
|
158
|
+
pool_recycle: int = 3600 # 1 hour
|
|
159
|
+
pool_pre_ping: bool = True
|
|
160
|
+
echo_pool: bool = False
|
|
161
|
+
reset_on_return: str = "rollback" # "rollback", "commit", or None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _get_default_retryable_errors() -> tuple[type[Exception], ...]:
|
|
165
|
+
"""Get default retryable error types based on available imports."""
|
|
166
|
+
if HAS_SQLALCHEMY:
|
|
167
|
+
return (OperationalError, DisconnectionError, InterfaceError)
|
|
168
|
+
# When SQLAlchemy is not available, use a placeholder that won't match anything
|
|
169
|
+
# Users should provide their own retryable_errors in this case
|
|
170
|
+
return ()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class RetryConfig:
|
|
175
|
+
"""Retry behavior configuration.
|
|
176
|
+
|
|
177
|
+
Attributes:
|
|
178
|
+
max_retries: Maximum number of retry attempts.
|
|
179
|
+
base_delay: Base delay between retries in seconds.
|
|
180
|
+
max_delay: Maximum delay between retries in seconds.
|
|
181
|
+
exponential_base: Base for exponential backoff calculation.
|
|
182
|
+
jitter: Whether to add random jitter to delays.
|
|
183
|
+
retryable_errors: Error types that should trigger retry.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
max_retries: int = 3
|
|
187
|
+
base_delay: float = 0.1
|
|
188
|
+
max_delay: float = 30.0
|
|
189
|
+
exponential_base: float = 2.0
|
|
190
|
+
jitter: bool = True
|
|
191
|
+
retryable_errors: tuple[type[Exception], ...] = field(
|
|
192
|
+
default_factory=_get_default_retryable_errors
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class CircuitBreakerConfig:
|
|
198
|
+
"""Circuit breaker configuration.
|
|
199
|
+
|
|
200
|
+
Attributes:
|
|
201
|
+
failure_threshold: Failures before opening circuit.
|
|
202
|
+
success_threshold: Successes before closing circuit.
|
|
203
|
+
timeout: Seconds before attempting recovery.
|
|
204
|
+
half_open_max_calls: Max calls in half-open state.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
failure_threshold: int = 5
|
|
208
|
+
success_threshold: int = 3
|
|
209
|
+
timeout: float = 60.0
|
|
210
|
+
half_open_max_calls: int = 3
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class HealthCheckConfig:
|
|
215
|
+
"""Health check configuration.
|
|
216
|
+
|
|
217
|
+
Attributes:
|
|
218
|
+
enabled: Whether health checks are enabled.
|
|
219
|
+
interval: Seconds between health checks.
|
|
220
|
+
timeout: Seconds before health check times out.
|
|
221
|
+
query: SQL query to use for health check.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
enabled: bool = True
|
|
225
|
+
interval: float = 30.0
|
|
226
|
+
timeout: float = 5.0
|
|
227
|
+
query: str = "SELECT 1"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class ConnectionPoolConfig:
|
|
232
|
+
"""Complete connection pool manager configuration.
|
|
233
|
+
|
|
234
|
+
Combines all sub-configurations into a single config object.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
connection_url: str = "sqlite:///:memory:"
|
|
238
|
+
pool: PoolConfig = field(default_factory=PoolConfig)
|
|
239
|
+
retry: RetryConfig = field(default_factory=RetryConfig)
|
|
240
|
+
circuit_breaker: CircuitBreakerConfig = field(default_factory=CircuitBreakerConfig)
|
|
241
|
+
health_check: HealthCheckConfig = field(default_factory=HealthCheckConfig)
|
|
242
|
+
echo: bool = False
|
|
243
|
+
connect_args: dict[str, Any] = field(default_factory=dict)
|
|
244
|
+
engine_options: dict[str, Any] = field(default_factory=dict)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def dialect(self) -> DatabaseDialect:
|
|
248
|
+
"""Get detected database dialect."""
|
|
249
|
+
return DatabaseDialect.from_url(self.connection_url)
|
|
250
|
+
|
|
251
|
+
def with_dialect_defaults(self) -> "ConnectionPoolConfig":
|
|
252
|
+
"""Apply dialect-specific default settings."""
|
|
253
|
+
dialect = self.dialect
|
|
254
|
+
config = ConnectionPoolConfig(
|
|
255
|
+
connection_url=self.connection_url,
|
|
256
|
+
pool=PoolConfig(
|
|
257
|
+
strategy=self.pool.strategy,
|
|
258
|
+
pool_size=self.pool.pool_size,
|
|
259
|
+
max_overflow=self.pool.max_overflow,
|
|
260
|
+
pool_timeout=self.pool.pool_timeout,
|
|
261
|
+
pool_recycle=self.pool.pool_recycle,
|
|
262
|
+
pool_pre_ping=self.pool.pool_pre_ping,
|
|
263
|
+
echo_pool=self.pool.echo_pool,
|
|
264
|
+
reset_on_return=self.pool.reset_on_return,
|
|
265
|
+
),
|
|
266
|
+
retry=self.retry,
|
|
267
|
+
circuit_breaker=self.circuit_breaker,
|
|
268
|
+
health_check=self.health_check,
|
|
269
|
+
echo=self.echo,
|
|
270
|
+
connect_args=dict(self.connect_args),
|
|
271
|
+
engine_options=dict(self.engine_options),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Apply dialect-specific defaults
|
|
275
|
+
if dialect == DatabaseDialect.SQLITE:
|
|
276
|
+
config.connect_args.setdefault("check_same_thread", False)
|
|
277
|
+
# SQLite doesn't support connection pooling well
|
|
278
|
+
if config.pool.strategy == PoolStrategy.QUEUE_POOL:
|
|
279
|
+
config.pool.pool_size = 1
|
|
280
|
+
config.pool.max_overflow = 0
|
|
281
|
+
|
|
282
|
+
elif dialect == DatabaseDialect.POSTGRESQL:
|
|
283
|
+
# PostgreSQL-specific optimizations
|
|
284
|
+
config.engine_options.setdefault("pool_use_lifo", True)
|
|
285
|
+
config.pool.pool_recycle = min(config.pool.pool_recycle, 1800)
|
|
286
|
+
|
|
287
|
+
elif dialect == DatabaseDialect.MYSQL:
|
|
288
|
+
# MySQL typically needs more aggressive recycling
|
|
289
|
+
config.pool.pool_recycle = min(config.pool.pool_recycle, 3600)
|
|
290
|
+
config.connect_args.setdefault("connect_timeout", 10)
|
|
291
|
+
|
|
292
|
+
return config
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# =============================================================================
|
|
296
|
+
# Protocols
|
|
297
|
+
# =============================================================================
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@runtime_checkable
|
|
301
|
+
class PoolMetricsProtocol(Protocol):
|
|
302
|
+
"""Protocol for pool metrics providers."""
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def total_connections(self) -> int:
|
|
306
|
+
"""Total connections in pool."""
|
|
307
|
+
...
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def active_connections(self) -> int:
|
|
311
|
+
"""Currently checked out connections."""
|
|
312
|
+
...
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def idle_connections(self) -> int:
|
|
316
|
+
"""Available connections in pool."""
|
|
317
|
+
...
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def overflow_connections(self) -> int:
|
|
321
|
+
"""Connections beyond pool_size."""
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@runtime_checkable
|
|
326
|
+
class ConnectionPoolProtocol(Protocol):
|
|
327
|
+
"""Protocol for connection pool implementations."""
|
|
328
|
+
|
|
329
|
+
def get_connection(self) -> Any:
|
|
330
|
+
"""Get a connection from the pool."""
|
|
331
|
+
...
|
|
332
|
+
|
|
333
|
+
def return_connection(self, connection: Any) -> None:
|
|
334
|
+
"""Return a connection to the pool."""
|
|
335
|
+
...
|
|
336
|
+
|
|
337
|
+
def dispose(self) -> None:
|
|
338
|
+
"""Dispose of all connections."""
|
|
339
|
+
...
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def metrics(self) -> PoolMetricsProtocol:
|
|
343
|
+
"""Get pool metrics."""
|
|
344
|
+
...
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# =============================================================================
|
|
348
|
+
# Metrics
|
|
349
|
+
# =============================================================================
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass
|
|
353
|
+
class PoolMetrics:
|
|
354
|
+
"""Connection pool metrics for monitoring and observability.
|
|
355
|
+
|
|
356
|
+
Thread-safe metrics collection with atomic operations.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
360
|
+
|
|
361
|
+
# Connection counts
|
|
362
|
+
total_created: int = 0
|
|
363
|
+
total_closed: int = 0
|
|
364
|
+
current_size: int = 0
|
|
365
|
+
current_checked_out: int = 0
|
|
366
|
+
current_overflow: int = 0
|
|
367
|
+
peak_connections: int = 0
|
|
368
|
+
|
|
369
|
+
# Operation counts
|
|
370
|
+
checkouts: int = 0
|
|
371
|
+
checkins: int = 0
|
|
372
|
+
checkout_failures: int = 0
|
|
373
|
+
recycles: int = 0
|
|
374
|
+
invalidations: int = 0
|
|
375
|
+
|
|
376
|
+
# Timing metrics
|
|
377
|
+
total_checkout_time_ms: float = 0.0
|
|
378
|
+
max_checkout_time_ms: float = 0.0
|
|
379
|
+
total_connection_time_ms: float = 0.0
|
|
380
|
+
|
|
381
|
+
# Health check metrics
|
|
382
|
+
health_checks_passed: int = 0
|
|
383
|
+
health_checks_failed: int = 0
|
|
384
|
+
last_health_check: datetime | None = None
|
|
385
|
+
|
|
386
|
+
# Circuit breaker metrics
|
|
387
|
+
circuit_opens: int = 0
|
|
388
|
+
circuit_closes: int = 0
|
|
389
|
+
requests_rejected: int = 0
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def total_connections(self) -> int:
|
|
393
|
+
"""Total connections currently managed."""
|
|
394
|
+
return self.current_size + self.current_overflow
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def active_connections(self) -> int:
|
|
398
|
+
"""Currently checked out connections."""
|
|
399
|
+
return self.current_checked_out
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def idle_connections(self) -> int:
|
|
403
|
+
"""Available connections in pool."""
|
|
404
|
+
return self.current_size - self.current_checked_out
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def overflow_connections(self) -> int:
|
|
408
|
+
"""Connections beyond pool_size."""
|
|
409
|
+
return self.current_overflow
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def avg_checkout_time_ms(self) -> float:
|
|
413
|
+
"""Average checkout time in milliseconds."""
|
|
414
|
+
if self.checkouts == 0:
|
|
415
|
+
return 0.0
|
|
416
|
+
return self.total_checkout_time_ms / self.checkouts
|
|
417
|
+
|
|
418
|
+
def record_checkout(self, duration_ms: float) -> None:
|
|
419
|
+
"""Record a successful checkout."""
|
|
420
|
+
with self._lock:
|
|
421
|
+
self.checkouts += 1
|
|
422
|
+
self.current_checked_out += 1
|
|
423
|
+
self.total_checkout_time_ms += duration_ms
|
|
424
|
+
self.max_checkout_time_ms = max(self.max_checkout_time_ms, duration_ms)
|
|
425
|
+
if self.current_checked_out > self.peak_connections:
|
|
426
|
+
self.peak_connections = self.current_checked_out
|
|
427
|
+
|
|
428
|
+
def record_checkin(self) -> None:
|
|
429
|
+
"""Record a connection checkin."""
|
|
430
|
+
with self._lock:
|
|
431
|
+
self.checkins += 1
|
|
432
|
+
self.current_checked_out = max(0, self.current_checked_out - 1)
|
|
433
|
+
|
|
434
|
+
def record_creation(self) -> None:
|
|
435
|
+
"""Record a new connection creation."""
|
|
436
|
+
with self._lock:
|
|
437
|
+
self.total_created += 1
|
|
438
|
+
self.current_size += 1
|
|
439
|
+
|
|
440
|
+
def record_close(self) -> None:
|
|
441
|
+
"""Record a connection close."""
|
|
442
|
+
with self._lock:
|
|
443
|
+
self.total_closed += 1
|
|
444
|
+
self.current_size = max(0, self.current_size - 1)
|
|
445
|
+
|
|
446
|
+
def record_health_check(self, passed: bool) -> None:
|
|
447
|
+
"""Record health check result."""
|
|
448
|
+
with self._lock:
|
|
449
|
+
self.last_health_check = datetime.now(timezone.utc)
|
|
450
|
+
if passed:
|
|
451
|
+
self.health_checks_passed += 1
|
|
452
|
+
else:
|
|
453
|
+
self.health_checks_failed += 1
|
|
454
|
+
|
|
455
|
+
def to_dict(self) -> dict[str, Any]:
|
|
456
|
+
"""Convert metrics to dictionary for serialization."""
|
|
457
|
+
return {
|
|
458
|
+
"connections": {
|
|
459
|
+
"total_created": self.total_created,
|
|
460
|
+
"total_closed": self.total_closed,
|
|
461
|
+
"current_size": self.current_size,
|
|
462
|
+
"current_checked_out": self.current_checked_out,
|
|
463
|
+
"current_overflow": self.current_overflow,
|
|
464
|
+
"peak": self.peak_connections,
|
|
465
|
+
"idle": self.idle_connections,
|
|
466
|
+
},
|
|
467
|
+
"operations": {
|
|
468
|
+
"checkouts": self.checkouts,
|
|
469
|
+
"checkins": self.checkins,
|
|
470
|
+
"checkout_failures": self.checkout_failures,
|
|
471
|
+
"recycles": self.recycles,
|
|
472
|
+
"invalidations": self.invalidations,
|
|
473
|
+
},
|
|
474
|
+
"timing": {
|
|
475
|
+
"avg_checkout_ms": round(self.avg_checkout_time_ms, 2),
|
|
476
|
+
"max_checkout_ms": round(self.max_checkout_time_ms, 2),
|
|
477
|
+
"total_connection_time_ms": round(self.total_connection_time_ms, 2),
|
|
478
|
+
},
|
|
479
|
+
"health": {
|
|
480
|
+
"checks_passed": self.health_checks_passed,
|
|
481
|
+
"checks_failed": self.health_checks_failed,
|
|
482
|
+
"last_check": (
|
|
483
|
+
self.last_health_check.isoformat() if self.last_health_check else None
|
|
484
|
+
),
|
|
485
|
+
},
|
|
486
|
+
"circuit_breaker": {
|
|
487
|
+
"opens": self.circuit_opens,
|
|
488
|
+
"closes": self.circuit_closes,
|
|
489
|
+
"requests_rejected": self.requests_rejected,
|
|
490
|
+
},
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
def to_prometheus(self) -> str:
|
|
494
|
+
"""Export metrics in Prometheus format."""
|
|
495
|
+
lines = [
|
|
496
|
+
"# HELP truthound_pool_connections_total Total connections created",
|
|
497
|
+
f"truthound_pool_connections_total {self.total_created}",
|
|
498
|
+
"# HELP truthound_pool_connections_current Current pool size",
|
|
499
|
+
f"truthound_pool_connections_current {self.current_size}",
|
|
500
|
+
"# HELP truthound_pool_connections_active Active connections",
|
|
501
|
+
f"truthound_pool_connections_active {self.current_checked_out}",
|
|
502
|
+
"# HELP truthound_pool_connections_idle Idle connections",
|
|
503
|
+
f"truthound_pool_connections_idle {self.idle_connections}",
|
|
504
|
+
"# HELP truthound_pool_checkouts_total Total checkouts",
|
|
505
|
+
f"truthound_pool_checkouts_total {self.checkouts}",
|
|
506
|
+
"# HELP truthound_pool_checkout_time_avg_ms Average checkout time",
|
|
507
|
+
f"truthound_pool_checkout_time_avg_ms {self.avg_checkout_time_ms:.2f}",
|
|
508
|
+
"# HELP truthound_pool_health_checks_passed Health checks passed",
|
|
509
|
+
f"truthound_pool_health_checks_passed {self.health_checks_passed}",
|
|
510
|
+
"# HELP truthound_pool_health_checks_failed Health checks failed",
|
|
511
|
+
f"truthound_pool_health_checks_failed {self.health_checks_failed}",
|
|
512
|
+
]
|
|
513
|
+
return "\n".join(lines)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# =============================================================================
|
|
517
|
+
# Circuit Breaker
|
|
518
|
+
# =============================================================================
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class CircuitBreaker:
|
|
522
|
+
"""Circuit breaker implementation for connection pool fault tolerance.
|
|
523
|
+
|
|
524
|
+
Prevents cascading failures by temporarily rejecting requests when
|
|
525
|
+
the database is experiencing issues.
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
def __init__(self, config: CircuitBreakerConfig) -> None:
|
|
529
|
+
"""Initialize circuit breaker.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
config: Circuit breaker configuration.
|
|
533
|
+
"""
|
|
534
|
+
self._config = config
|
|
535
|
+
self._state = CircuitState.CLOSED
|
|
536
|
+
self._failure_count = 0
|
|
537
|
+
self._success_count = 0
|
|
538
|
+
self._last_failure_time: datetime | None = None
|
|
539
|
+
self._half_open_calls = 0
|
|
540
|
+
self._lock = threading.Lock()
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def state(self) -> CircuitState:
|
|
544
|
+
"""Get current circuit state."""
|
|
545
|
+
return self._state
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def is_closed(self) -> bool:
|
|
549
|
+
"""Check if circuit is closed (normal operation)."""
|
|
550
|
+
return self._state == CircuitState.CLOSED
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def is_open(self) -> bool:
|
|
554
|
+
"""Check if circuit is open (rejecting requests)."""
|
|
555
|
+
return self._state == CircuitState.OPEN
|
|
556
|
+
|
|
557
|
+
def can_execute(self) -> bool:
|
|
558
|
+
"""Check if a request can be executed."""
|
|
559
|
+
with self._lock:
|
|
560
|
+
if self._state == CircuitState.CLOSED:
|
|
561
|
+
return True
|
|
562
|
+
|
|
563
|
+
if self._state == CircuitState.OPEN:
|
|
564
|
+
# Check if timeout has passed
|
|
565
|
+
if self._last_failure_time:
|
|
566
|
+
elapsed = (datetime.now(timezone.utc) - self._last_failure_time).total_seconds()
|
|
567
|
+
if elapsed >= self._config.timeout:
|
|
568
|
+
self._transition_to(CircuitState.HALF_OPEN)
|
|
569
|
+
# First call in half-open, increment counter
|
|
570
|
+
self._half_open_calls += 1
|
|
571
|
+
return True
|
|
572
|
+
return False
|
|
573
|
+
|
|
574
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
575
|
+
# Allow limited calls in half-open state
|
|
576
|
+
if self._half_open_calls < self._config.half_open_max_calls:
|
|
577
|
+
self._half_open_calls += 1
|
|
578
|
+
return True
|
|
579
|
+
return False
|
|
580
|
+
|
|
581
|
+
return False
|
|
582
|
+
|
|
583
|
+
def record_success(self) -> None:
|
|
584
|
+
"""Record a successful operation."""
|
|
585
|
+
with self._lock:
|
|
586
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
587
|
+
self._success_count += 1
|
|
588
|
+
if self._success_count >= self._config.success_threshold:
|
|
589
|
+
self._transition_to(CircuitState.CLOSED)
|
|
590
|
+
elif self._state == CircuitState.CLOSED:
|
|
591
|
+
# Reset failure count on success
|
|
592
|
+
self._failure_count = 0
|
|
593
|
+
|
|
594
|
+
def record_failure(self) -> None:
|
|
595
|
+
"""Record a failed operation."""
|
|
596
|
+
with self._lock:
|
|
597
|
+
self._failure_count += 1
|
|
598
|
+
self._last_failure_time = datetime.now(timezone.utc)
|
|
599
|
+
|
|
600
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
601
|
+
# Any failure in half-open immediately opens
|
|
602
|
+
self._transition_to(CircuitState.OPEN)
|
|
603
|
+
elif self._state == CircuitState.CLOSED:
|
|
604
|
+
if self._failure_count >= self._config.failure_threshold:
|
|
605
|
+
self._transition_to(CircuitState.OPEN)
|
|
606
|
+
|
|
607
|
+
def _transition_to(self, new_state: CircuitState) -> None:
|
|
608
|
+
"""Transition to a new state."""
|
|
609
|
+
old_state = self._state
|
|
610
|
+
self._state = new_state
|
|
611
|
+
|
|
612
|
+
if new_state == CircuitState.CLOSED:
|
|
613
|
+
self._failure_count = 0
|
|
614
|
+
self._success_count = 0
|
|
615
|
+
self._half_open_calls = 0
|
|
616
|
+
logger.info(f"Circuit breaker closed (was {old_state.name})")
|
|
617
|
+
|
|
618
|
+
elif new_state == CircuitState.OPEN:
|
|
619
|
+
self._success_count = 0
|
|
620
|
+
self._half_open_calls = 0
|
|
621
|
+
logger.warning(
|
|
622
|
+
f"Circuit breaker opened after {self._failure_count} failures"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
elif new_state == CircuitState.HALF_OPEN:
|
|
626
|
+
self._success_count = 0
|
|
627
|
+
self._half_open_calls = 0
|
|
628
|
+
logger.info("Circuit breaker half-open, testing recovery")
|
|
629
|
+
|
|
630
|
+
def reset(self) -> None:
|
|
631
|
+
"""Reset circuit breaker to closed state."""
|
|
632
|
+
with self._lock:
|
|
633
|
+
self._transition_to(CircuitState.CLOSED)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# =============================================================================
|
|
637
|
+
# Retry Handler
|
|
638
|
+
# =============================================================================
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class RetryHandler:
|
|
642
|
+
"""Retry handler with exponential backoff and jitter.
|
|
643
|
+
|
|
644
|
+
Provides configurable retry behavior for transient database errors.
|
|
645
|
+
"""
|
|
646
|
+
|
|
647
|
+
def __init__(self, config: RetryConfig) -> None:
|
|
648
|
+
"""Initialize retry handler.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
config: Retry configuration.
|
|
652
|
+
"""
|
|
653
|
+
self._config = config
|
|
654
|
+
import random
|
|
655
|
+
|
|
656
|
+
self._random = random
|
|
657
|
+
|
|
658
|
+
def should_retry(self, error: Exception, attempt: int) -> bool:
|
|
659
|
+
"""Check if operation should be retried.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
error: The exception that occurred.
|
|
663
|
+
attempt: Current attempt number (1-based).
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
True if should retry, False otherwise.
|
|
667
|
+
"""
|
|
668
|
+
if attempt >= self._config.max_retries:
|
|
669
|
+
return False
|
|
670
|
+
return isinstance(error, self._config.retryable_errors)
|
|
671
|
+
|
|
672
|
+
def get_delay(self, attempt: int) -> float:
|
|
673
|
+
"""Calculate delay before next retry.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
attempt: Current attempt number (1-based).
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
Delay in seconds.
|
|
680
|
+
"""
|
|
681
|
+
delay = self._config.base_delay * (self._config.exponential_base ** (attempt - 1))
|
|
682
|
+
delay = min(delay, self._config.max_delay)
|
|
683
|
+
|
|
684
|
+
if self._config.jitter:
|
|
685
|
+
# Add ±25% jitter
|
|
686
|
+
jitter = delay * 0.25 * (2 * self._random.random() - 1)
|
|
687
|
+
delay += jitter
|
|
688
|
+
|
|
689
|
+
return max(0, delay)
|
|
690
|
+
|
|
691
|
+
def execute_with_retry(
|
|
692
|
+
self,
|
|
693
|
+
operation: Callable[[], Any],
|
|
694
|
+
on_retry: Callable[[Exception, int], None] | None = None,
|
|
695
|
+
) -> Any:
|
|
696
|
+
"""Execute operation with retry logic.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
operation: Callable to execute.
|
|
700
|
+
on_retry: Optional callback called before each retry.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Result of the operation.
|
|
704
|
+
|
|
705
|
+
Raises:
|
|
706
|
+
Exception: The last exception if all retries exhausted.
|
|
707
|
+
"""
|
|
708
|
+
last_error: Exception | None = None
|
|
709
|
+
|
|
710
|
+
for attempt in range(1, self._config.max_retries + 2):
|
|
711
|
+
try:
|
|
712
|
+
return operation()
|
|
713
|
+
except Exception as e:
|
|
714
|
+
last_error = e
|
|
715
|
+
|
|
716
|
+
if not self.should_retry(e, attempt):
|
|
717
|
+
raise
|
|
718
|
+
|
|
719
|
+
delay = self.get_delay(attempt)
|
|
720
|
+
|
|
721
|
+
if on_retry:
|
|
722
|
+
on_retry(e, attempt)
|
|
723
|
+
|
|
724
|
+
logger.warning(
|
|
725
|
+
f"Retry attempt {attempt}/{self._config.max_retries} "
|
|
726
|
+
f"after {delay:.2f}s: {e}"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
time.sleep(delay)
|
|
730
|
+
|
|
731
|
+
# Should not reach here, but for type safety
|
|
732
|
+
if last_error:
|
|
733
|
+
raise last_error
|
|
734
|
+
raise RuntimeError("Retry logic error")
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# =============================================================================
|
|
738
|
+
# Connection Pool Manager
|
|
739
|
+
# =============================================================================
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
class ConnectionPoolManager:
|
|
743
|
+
"""Enterprise-grade connection pool manager.
|
|
744
|
+
|
|
745
|
+
Provides:
|
|
746
|
+
- Multiple pooling strategies
|
|
747
|
+
- Health monitoring
|
|
748
|
+
- Circuit breaker protection
|
|
749
|
+
- Retry logic
|
|
750
|
+
- Comprehensive metrics
|
|
751
|
+
- Database-specific optimizations
|
|
752
|
+
|
|
753
|
+
Example:
|
|
754
|
+
>>> config = ConnectionPoolConfig(
|
|
755
|
+
... connection_url="postgresql://user:pass@localhost/db",
|
|
756
|
+
... pool=PoolConfig(pool_size=10, max_overflow=20),
|
|
757
|
+
... )
|
|
758
|
+
>>> manager = ConnectionPoolManager(config)
|
|
759
|
+
>>> with manager.session() as session:
|
|
760
|
+
... session.execute(text("SELECT 1"))
|
|
761
|
+
"""
|
|
762
|
+
|
|
763
|
+
def __init__(
|
|
764
|
+
self,
|
|
765
|
+
config: ConnectionPoolConfig | None = None,
|
|
766
|
+
*,
|
|
767
|
+
connection_url: str | None = None,
|
|
768
|
+
pool_size: int = 5,
|
|
769
|
+
max_overflow: int = 10,
|
|
770
|
+
**kwargs: Any,
|
|
771
|
+
) -> None:
|
|
772
|
+
"""Initialize connection pool manager.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
config: Complete configuration object.
|
|
776
|
+
connection_url: Database connection URL (if not using config).
|
|
777
|
+
pool_size: Pool size (if not using config).
|
|
778
|
+
max_overflow: Max overflow (if not using config).
|
|
779
|
+
**kwargs: Additional options passed to config.
|
|
780
|
+
"""
|
|
781
|
+
if not HAS_SQLALCHEMY:
|
|
782
|
+
raise ImportError(
|
|
783
|
+
"SQLAlchemy is required for ConnectionPoolManager. "
|
|
784
|
+
"Install with: pip install truthound[database]"
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
# Build config from parameters if not provided
|
|
788
|
+
if config is None:
|
|
789
|
+
url = connection_url or kwargs.pop("url", "sqlite:///:memory:")
|
|
790
|
+
pool_config = PoolConfig(
|
|
791
|
+
pool_size=pool_size,
|
|
792
|
+
max_overflow=max_overflow,
|
|
793
|
+
pool_timeout=kwargs.pop("pool_timeout", 30.0),
|
|
794
|
+
pool_recycle=kwargs.pop("pool_recycle", 3600),
|
|
795
|
+
pool_pre_ping=kwargs.pop("pool_pre_ping", True),
|
|
796
|
+
)
|
|
797
|
+
config = ConnectionPoolConfig(
|
|
798
|
+
connection_url=url,
|
|
799
|
+
pool=pool_config,
|
|
800
|
+
echo=kwargs.pop("echo", False),
|
|
801
|
+
connect_args=kwargs.pop("connect_args", {}),
|
|
802
|
+
engine_options=kwargs,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# Apply dialect-specific defaults
|
|
806
|
+
self._config = config.with_dialect_defaults()
|
|
807
|
+
|
|
808
|
+
# Initialize components
|
|
809
|
+
self._engine: Engine | None = None
|
|
810
|
+
self._session_factory: sessionmaker | None = None
|
|
811
|
+
self._metrics = PoolMetrics()
|
|
812
|
+
self._circuit_breaker = CircuitBreaker(self._config.circuit_breaker)
|
|
813
|
+
self._retry_handler = RetryHandler(self._config.retry)
|
|
814
|
+
|
|
815
|
+
# Health check management
|
|
816
|
+
self._health_check_thread: threading.Thread | None = None
|
|
817
|
+
self._health_check_stop = threading.Event()
|
|
818
|
+
self._is_healthy = True
|
|
819
|
+
|
|
820
|
+
# Lifecycle management
|
|
821
|
+
self._lock = threading.RLock()
|
|
822
|
+
self._initialized = False
|
|
823
|
+
self._disposed = False
|
|
824
|
+
|
|
825
|
+
# Tracked sessions for cleanup
|
|
826
|
+
self._active_sessions: weakref.WeakSet[Session] = weakref.WeakSet()
|
|
827
|
+
|
|
828
|
+
@property
|
|
829
|
+
def config(self) -> ConnectionPoolConfig:
|
|
830
|
+
"""Get pool configuration."""
|
|
831
|
+
return self._config
|
|
832
|
+
|
|
833
|
+
@property
|
|
834
|
+
def metrics(self) -> PoolMetrics:
|
|
835
|
+
"""Get pool metrics."""
|
|
836
|
+
return self._metrics
|
|
837
|
+
|
|
838
|
+
@property
|
|
839
|
+
def is_healthy(self) -> bool:
|
|
840
|
+
"""Check if pool is healthy."""
|
|
841
|
+
return self._is_healthy and self._circuit_breaker.is_closed
|
|
842
|
+
|
|
843
|
+
@property
|
|
844
|
+
def circuit_state(self) -> CircuitState:
|
|
845
|
+
"""Get circuit breaker state."""
|
|
846
|
+
return self._circuit_breaker.state
|
|
847
|
+
|
|
848
|
+
def initialize(self) -> None:
|
|
849
|
+
"""Initialize the connection pool.
|
|
850
|
+
|
|
851
|
+
Creates the engine, session factory, and starts health checks.
|
|
852
|
+
"""
|
|
853
|
+
with self._lock:
|
|
854
|
+
if self._initialized:
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
if self._disposed:
|
|
858
|
+
raise RuntimeError("Cannot initialize disposed pool manager")
|
|
859
|
+
|
|
860
|
+
self._create_engine()
|
|
861
|
+
self._test_connection()
|
|
862
|
+
self._setup_event_listeners()
|
|
863
|
+
self._start_health_checks()
|
|
864
|
+
self._initialized = True
|
|
865
|
+
|
|
866
|
+
logger.info(
|
|
867
|
+
f"ConnectionPoolManager initialized: "
|
|
868
|
+
f"dialect={self._config.dialect.value}, "
|
|
869
|
+
f"pool_size={self._config.pool.pool_size}, "
|
|
870
|
+
f"max_overflow={self._config.pool.max_overflow}"
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
def _create_engine(self) -> None:
|
|
874
|
+
"""Create SQLAlchemy engine with configured pool."""
|
|
875
|
+
pool_config = self._config.pool
|
|
876
|
+
strategy = pool_config.strategy
|
|
877
|
+
|
|
878
|
+
# Build pool class based on strategy
|
|
879
|
+
pool_class: type | None = None
|
|
880
|
+
pool_kwargs: dict[str, Any] = {}
|
|
881
|
+
|
|
882
|
+
if strategy == PoolStrategy.QUEUE_POOL:
|
|
883
|
+
pool_class = QueuePool
|
|
884
|
+
pool_kwargs = {
|
|
885
|
+
"pool_size": pool_config.pool_size,
|
|
886
|
+
"max_overflow": pool_config.max_overflow,
|
|
887
|
+
"timeout": pool_config.pool_timeout,
|
|
888
|
+
"pool_recycle": pool_config.pool_recycle,
|
|
889
|
+
"pool_pre_ping": pool_config.pool_pre_ping,
|
|
890
|
+
}
|
|
891
|
+
elif strategy == PoolStrategy.NULL_POOL:
|
|
892
|
+
pool_class = NullPool
|
|
893
|
+
elif strategy == PoolStrategy.STATIC_POOL:
|
|
894
|
+
pool_class = StaticPool
|
|
895
|
+
elif strategy == PoolStrategy.SINGLETON_THREAD:
|
|
896
|
+
pool_class = SingletonThreadPool
|
|
897
|
+
pool_kwargs = {"pool_size": pool_config.pool_size}
|
|
898
|
+
|
|
899
|
+
# Build engine options
|
|
900
|
+
engine_options = {
|
|
901
|
+
"echo": self._config.echo,
|
|
902
|
+
"echo_pool": pool_config.echo_pool,
|
|
903
|
+
"connect_args": self._config.connect_args,
|
|
904
|
+
**self._config.engine_options,
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if pool_class:
|
|
908
|
+
engine_options["poolclass"] = pool_class
|
|
909
|
+
engine_options.update(pool_kwargs)
|
|
910
|
+
|
|
911
|
+
self._engine = create_engine(self._config.connection_url, **engine_options)
|
|
912
|
+
self._session_factory = sessionmaker(bind=self._engine)
|
|
913
|
+
|
|
914
|
+
def _test_connection(self) -> None:
|
|
915
|
+
"""Test database connection."""
|
|
916
|
+
if self._engine is None:
|
|
917
|
+
raise RuntimeError("Engine not created")
|
|
918
|
+
|
|
919
|
+
try:
|
|
920
|
+
with self._engine.connect() as conn:
|
|
921
|
+
conn.execute(text(self._config.health_check.query))
|
|
922
|
+
conn.commit()
|
|
923
|
+
except SQLAlchemyError as e:
|
|
924
|
+
logger.error(f"Connection test failed: {e}")
|
|
925
|
+
raise
|
|
926
|
+
|
|
927
|
+
def _setup_event_listeners(self) -> None:
|
|
928
|
+
"""Set up SQLAlchemy event listeners for metrics."""
|
|
929
|
+
if self._engine is None:
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
@event.listens_for(self._engine, "checkout")
|
|
933
|
+
def on_checkout(
|
|
934
|
+
dbapi_conn: Any, connection_record: Any, connection_proxy: Any
|
|
935
|
+
) -> None:
|
|
936
|
+
self._metrics.record_checkout(0) # Duration tracked elsewhere
|
|
937
|
+
|
|
938
|
+
@event.listens_for(self._engine, "checkin")
|
|
939
|
+
def on_checkin(dbapi_conn: Any, connection_record: Any) -> None:
|
|
940
|
+
self._metrics.record_checkin()
|
|
941
|
+
|
|
942
|
+
@event.listens_for(self._engine, "connect")
|
|
943
|
+
def on_connect(dbapi_conn: Any, connection_record: Any) -> None:
|
|
944
|
+
self._metrics.record_creation()
|
|
945
|
+
|
|
946
|
+
@event.listens_for(self._engine, "close")
|
|
947
|
+
def on_close(dbapi_conn: Any, connection_record: Any) -> None:
|
|
948
|
+
self._metrics.record_close()
|
|
949
|
+
|
|
950
|
+
@event.listens_for(self._engine, "invalidate")
|
|
951
|
+
def on_invalidate(
|
|
952
|
+
dbapi_conn: Any, connection_record: Any, exception: Any
|
|
953
|
+
) -> None:
|
|
954
|
+
self._metrics._lock.acquire()
|
|
955
|
+
try:
|
|
956
|
+
self._metrics.invalidations += 1
|
|
957
|
+
finally:
|
|
958
|
+
self._metrics._lock.release()
|
|
959
|
+
|
|
960
|
+
def _start_health_checks(self) -> None:
|
|
961
|
+
"""Start background health check thread."""
|
|
962
|
+
if not self._config.health_check.enabled:
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
self._health_check_stop.clear()
|
|
966
|
+
self._health_check_thread = threading.Thread(
|
|
967
|
+
target=self._health_check_loop,
|
|
968
|
+
name="truthound-pool-health-check",
|
|
969
|
+
daemon=True,
|
|
970
|
+
)
|
|
971
|
+
self._health_check_thread.start()
|
|
972
|
+
|
|
973
|
+
def _health_check_loop(self) -> None:
|
|
974
|
+
"""Background health check loop."""
|
|
975
|
+
while not self._health_check_stop.is_set():
|
|
976
|
+
try:
|
|
977
|
+
self._perform_health_check()
|
|
978
|
+
except Exception as e:
|
|
979
|
+
logger.error(f"Health check error: {e}")
|
|
980
|
+
|
|
981
|
+
self._health_check_stop.wait(self._config.health_check.interval)
|
|
982
|
+
|
|
983
|
+
def _perform_health_check(self) -> None:
|
|
984
|
+
"""Perform a single health check."""
|
|
985
|
+
if self._engine is None:
|
|
986
|
+
self._is_healthy = False
|
|
987
|
+
self._metrics.record_health_check(False)
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
try:
|
|
991
|
+
with self._engine.connect() as conn:
|
|
992
|
+
conn.execute(text(self._config.health_check.query))
|
|
993
|
+
conn.commit()
|
|
994
|
+
self._is_healthy = True
|
|
995
|
+
self._metrics.record_health_check(True)
|
|
996
|
+
except SQLAlchemyError:
|
|
997
|
+
self._is_healthy = False
|
|
998
|
+
self._metrics.record_health_check(False)
|
|
999
|
+
|
|
1000
|
+
def get_engine(self) -> "Engine":
|
|
1001
|
+
"""Get the SQLAlchemy engine.
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
The configured engine.
|
|
1005
|
+
|
|
1006
|
+
Raises:
|
|
1007
|
+
RuntimeError: If not initialized.
|
|
1008
|
+
"""
|
|
1009
|
+
if not self._initialized:
|
|
1010
|
+
self.initialize()
|
|
1011
|
+
if self._engine is None:
|
|
1012
|
+
raise RuntimeError("Engine not available")
|
|
1013
|
+
return self._engine
|
|
1014
|
+
|
|
1015
|
+
def get_session(self) -> "Session":
|
|
1016
|
+
"""Get a new database session.
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
A new session instance.
|
|
1020
|
+
|
|
1021
|
+
Raises:
|
|
1022
|
+
RuntimeError: If circuit is open.
|
|
1023
|
+
"""
|
|
1024
|
+
if not self._initialized:
|
|
1025
|
+
self.initialize()
|
|
1026
|
+
|
|
1027
|
+
if not self._circuit_breaker.can_execute():
|
|
1028
|
+
self._metrics._lock.acquire()
|
|
1029
|
+
try:
|
|
1030
|
+
self._metrics.requests_rejected += 1
|
|
1031
|
+
finally:
|
|
1032
|
+
self._metrics._lock.release()
|
|
1033
|
+
raise RuntimeError(
|
|
1034
|
+
"Circuit breaker is open, database connections temporarily unavailable"
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
if self._session_factory is None:
|
|
1038
|
+
raise RuntimeError("Session factory not available")
|
|
1039
|
+
|
|
1040
|
+
session = self._session_factory()
|
|
1041
|
+
self._active_sessions.add(session)
|
|
1042
|
+
return session
|
|
1043
|
+
|
|
1044
|
+
@contextmanager
|
|
1045
|
+
def session(self) -> Iterator["Session"]:
|
|
1046
|
+
"""Context manager for database sessions.
|
|
1047
|
+
|
|
1048
|
+
Provides automatic commit/rollback and connection return.
|
|
1049
|
+
|
|
1050
|
+
Yields:
|
|
1051
|
+
Database session.
|
|
1052
|
+
|
|
1053
|
+
Example:
|
|
1054
|
+
>>> with pool_manager.session() as session:
|
|
1055
|
+
... result = session.execute(text("SELECT 1"))
|
|
1056
|
+
"""
|
|
1057
|
+
session = None
|
|
1058
|
+
start_time = time.time()
|
|
1059
|
+
success = False
|
|
1060
|
+
|
|
1061
|
+
try:
|
|
1062
|
+
session = self.get_session()
|
|
1063
|
+
yield session
|
|
1064
|
+
session.commit()
|
|
1065
|
+
success = True
|
|
1066
|
+
self._circuit_breaker.record_success()
|
|
1067
|
+
|
|
1068
|
+
except SQLAlchemyError as e:
|
|
1069
|
+
if session:
|
|
1070
|
+
session.rollback()
|
|
1071
|
+
self._circuit_breaker.record_failure()
|
|
1072
|
+
raise
|
|
1073
|
+
|
|
1074
|
+
finally:
|
|
1075
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
1076
|
+
if session:
|
|
1077
|
+
try:
|
|
1078
|
+
session.close()
|
|
1079
|
+
except Exception:
|
|
1080
|
+
pass
|
|
1081
|
+
|
|
1082
|
+
# Record checkout timing
|
|
1083
|
+
if success:
|
|
1084
|
+
with self._metrics._lock:
|
|
1085
|
+
self._metrics.total_checkout_time_ms += duration_ms
|
|
1086
|
+
self._metrics.max_checkout_time_ms = max(
|
|
1087
|
+
self._metrics.max_checkout_time_ms, duration_ms
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
def execute_with_retry(
|
|
1091
|
+
self,
|
|
1092
|
+
operation: Callable[["Session"], Any],
|
|
1093
|
+
) -> Any:
|
|
1094
|
+
"""Execute operation with automatic retry.
|
|
1095
|
+
|
|
1096
|
+
Args:
|
|
1097
|
+
operation: Callable that takes a session and returns a result.
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Result of the operation.
|
|
1101
|
+
"""
|
|
1102
|
+
|
|
1103
|
+
def wrapped_operation() -> Any:
|
|
1104
|
+
with self.session() as session:
|
|
1105
|
+
return operation(session)
|
|
1106
|
+
|
|
1107
|
+
return self._retry_handler.execute_with_retry(wrapped_operation)
|
|
1108
|
+
|
|
1109
|
+
def recycle_connections(self) -> int:
|
|
1110
|
+
"""Manually recycle all pool connections.
|
|
1111
|
+
|
|
1112
|
+
Returns:
|
|
1113
|
+
Number of connections recycled.
|
|
1114
|
+
"""
|
|
1115
|
+
if self._engine is None:
|
|
1116
|
+
return 0
|
|
1117
|
+
|
|
1118
|
+
pool = self._engine.pool
|
|
1119
|
+
if pool is None:
|
|
1120
|
+
return 0
|
|
1121
|
+
|
|
1122
|
+
# Dispose and recreate
|
|
1123
|
+
old_size = self._metrics.current_size
|
|
1124
|
+
self._engine.dispose()
|
|
1125
|
+
self._metrics._lock.acquire()
|
|
1126
|
+
try:
|
|
1127
|
+
self._metrics.recycles += old_size
|
|
1128
|
+
self._metrics.current_size = 0
|
|
1129
|
+
finally:
|
|
1130
|
+
self._metrics._lock.release()
|
|
1131
|
+
|
|
1132
|
+
logger.info(f"Recycled {old_size} connections")
|
|
1133
|
+
return old_size
|
|
1134
|
+
|
|
1135
|
+
def get_pool_status(self) -> dict[str, Any]:
|
|
1136
|
+
"""Get comprehensive pool status.
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
Dictionary with pool status information.
|
|
1140
|
+
"""
|
|
1141
|
+
return {
|
|
1142
|
+
"initialized": self._initialized,
|
|
1143
|
+
"disposed": self._disposed,
|
|
1144
|
+
"healthy": self.is_healthy,
|
|
1145
|
+
"circuit_state": self._circuit_breaker.state.name,
|
|
1146
|
+
"dialect": self._config.dialect.value,
|
|
1147
|
+
"config": {
|
|
1148
|
+
"connection_url": self._mask_password(self._config.connection_url),
|
|
1149
|
+
"pool_size": self._config.pool.pool_size,
|
|
1150
|
+
"max_overflow": self._config.pool.max_overflow,
|
|
1151
|
+
"pool_timeout": self._config.pool.pool_timeout,
|
|
1152
|
+
"pool_recycle": self._config.pool.pool_recycle,
|
|
1153
|
+
},
|
|
1154
|
+
"metrics": self._metrics.to_dict(),
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
def _mask_password(self, url: str) -> str:
|
|
1158
|
+
"""Mask password in connection URL."""
|
|
1159
|
+
import re
|
|
1160
|
+
|
|
1161
|
+
return re.sub(r"://([^:]+):([^@]+)@", r"://\1:***@", url)
|
|
1162
|
+
|
|
1163
|
+
def dispose(self) -> None:
|
|
1164
|
+
"""Dispose of the connection pool.
|
|
1165
|
+
|
|
1166
|
+
Closes all connections and stops health checks.
|
|
1167
|
+
"""
|
|
1168
|
+
with self._lock:
|
|
1169
|
+
if self._disposed:
|
|
1170
|
+
return
|
|
1171
|
+
|
|
1172
|
+
# Stop health checks
|
|
1173
|
+
if self._health_check_thread:
|
|
1174
|
+
self._health_check_stop.set()
|
|
1175
|
+
self._health_check_thread.join(timeout=5.0)
|
|
1176
|
+
self._health_check_thread = None
|
|
1177
|
+
|
|
1178
|
+
# Close active sessions
|
|
1179
|
+
for session in list(self._active_sessions):
|
|
1180
|
+
try:
|
|
1181
|
+
session.close()
|
|
1182
|
+
except Exception:
|
|
1183
|
+
pass
|
|
1184
|
+
|
|
1185
|
+
# Dispose engine
|
|
1186
|
+
if self._engine:
|
|
1187
|
+
self._engine.dispose()
|
|
1188
|
+
self._engine = None
|
|
1189
|
+
self._session_factory = None
|
|
1190
|
+
|
|
1191
|
+
self._disposed = True
|
|
1192
|
+
self._initialized = False
|
|
1193
|
+
|
|
1194
|
+
logger.info("ConnectionPoolManager disposed")
|
|
1195
|
+
|
|
1196
|
+
def __enter__(self) -> "ConnectionPoolManager":
|
|
1197
|
+
"""Context manager entry."""
|
|
1198
|
+
self.initialize()
|
|
1199
|
+
return self
|
|
1200
|
+
|
|
1201
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1202
|
+
"""Context manager exit."""
|
|
1203
|
+
self.dispose()
|
|
1204
|
+
|
|
1205
|
+
def __del__(self) -> None:
|
|
1206
|
+
"""Destructor - ensure cleanup."""
|
|
1207
|
+
try:
|
|
1208
|
+
self.dispose()
|
|
1209
|
+
except Exception:
|
|
1210
|
+
pass
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
# =============================================================================
|
|
1214
|
+
# Factory Functions
|
|
1215
|
+
# =============================================================================
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def create_pool_manager(
|
|
1219
|
+
connection_url: str,
|
|
1220
|
+
*,
|
|
1221
|
+
pool_size: int = 5,
|
|
1222
|
+
max_overflow: int = 10,
|
|
1223
|
+
strategy: PoolStrategy = PoolStrategy.QUEUE_POOL,
|
|
1224
|
+
enable_circuit_breaker: bool = True,
|
|
1225
|
+
enable_health_checks: bool = True,
|
|
1226
|
+
**kwargs: Any,
|
|
1227
|
+
) -> ConnectionPoolManager:
|
|
1228
|
+
"""Create a connection pool manager with sensible defaults.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
connection_url: Database connection URL.
|
|
1232
|
+
pool_size: Number of connections to maintain.
|
|
1233
|
+
max_overflow: Maximum overflow connections.
|
|
1234
|
+
strategy: Pooling strategy to use.
|
|
1235
|
+
enable_circuit_breaker: Whether to enable circuit breaker.
|
|
1236
|
+
enable_health_checks: Whether to enable health checks.
|
|
1237
|
+
**kwargs: Additional configuration options.
|
|
1238
|
+
|
|
1239
|
+
Returns:
|
|
1240
|
+
Configured ConnectionPoolManager instance.
|
|
1241
|
+
|
|
1242
|
+
Example:
|
|
1243
|
+
>>> manager = create_pool_manager(
|
|
1244
|
+
... "postgresql://user:pass@localhost/db",
|
|
1245
|
+
... pool_size=10,
|
|
1246
|
+
... max_overflow=20,
|
|
1247
|
+
... )
|
|
1248
|
+
"""
|
|
1249
|
+
pool_config = PoolConfig(
|
|
1250
|
+
strategy=strategy,
|
|
1251
|
+
pool_size=pool_size,
|
|
1252
|
+
max_overflow=max_overflow,
|
|
1253
|
+
pool_timeout=kwargs.pop("pool_timeout", 30.0),
|
|
1254
|
+
pool_recycle=kwargs.pop("pool_recycle", 3600),
|
|
1255
|
+
pool_pre_ping=kwargs.pop("pool_pre_ping", True),
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
circuit_config = CircuitBreakerConfig()
|
|
1259
|
+
if not enable_circuit_breaker:
|
|
1260
|
+
circuit_config.failure_threshold = 999999 # Effectively disabled
|
|
1261
|
+
|
|
1262
|
+
health_config = HealthCheckConfig(enabled=enable_health_checks)
|
|
1263
|
+
|
|
1264
|
+
config = ConnectionPoolConfig(
|
|
1265
|
+
connection_url=connection_url,
|
|
1266
|
+
pool=pool_config,
|
|
1267
|
+
circuit_breaker=circuit_config,
|
|
1268
|
+
health_check=health_config,
|
|
1269
|
+
echo=kwargs.pop("echo", False),
|
|
1270
|
+
connect_args=kwargs.pop("connect_args", {}),
|
|
1271
|
+
engine_options=kwargs,
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
return ConnectionPoolManager(config)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def create_pool_for_dialect(
|
|
1278
|
+
dialect: DatabaseDialect | str,
|
|
1279
|
+
host: str = "localhost",
|
|
1280
|
+
port: int | None = None,
|
|
1281
|
+
database: str = "truthound",
|
|
1282
|
+
username: str = "",
|
|
1283
|
+
password: str = "",
|
|
1284
|
+
**kwargs: Any,
|
|
1285
|
+
) -> ConnectionPoolManager:
|
|
1286
|
+
"""Create a pool manager for a specific database dialect.
|
|
1287
|
+
|
|
1288
|
+
Args:
|
|
1289
|
+
dialect: Database dialect (or string name).
|
|
1290
|
+
host: Database host.
|
|
1291
|
+
port: Database port (uses dialect default if None).
|
|
1292
|
+
database: Database name.
|
|
1293
|
+
username: Database username.
|
|
1294
|
+
password: Database password.
|
|
1295
|
+
**kwargs: Additional pool configuration.
|
|
1296
|
+
|
|
1297
|
+
Returns:
|
|
1298
|
+
Configured ConnectionPoolManager instance.
|
|
1299
|
+
|
|
1300
|
+
Example:
|
|
1301
|
+
>>> manager = create_pool_for_dialect(
|
|
1302
|
+
... DatabaseDialect.POSTGRESQL,
|
|
1303
|
+
... host="localhost",
|
|
1304
|
+
... database="mydb",
|
|
1305
|
+
... username="user",
|
|
1306
|
+
... password="pass",
|
|
1307
|
+
... )
|
|
1308
|
+
"""
|
|
1309
|
+
if isinstance(dialect, str):
|
|
1310
|
+
dialect = DatabaseDialect(dialect)
|
|
1311
|
+
|
|
1312
|
+
# Build connection URL based on dialect
|
|
1313
|
+
default_ports = {
|
|
1314
|
+
DatabaseDialect.POSTGRESQL: 5432,
|
|
1315
|
+
DatabaseDialect.MYSQL: 3306,
|
|
1316
|
+
DatabaseDialect.MSSQL: 1433,
|
|
1317
|
+
DatabaseDialect.ORACLE: 1521,
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
port = port or default_ports.get(dialect)
|
|
1321
|
+
|
|
1322
|
+
if dialect == DatabaseDialect.SQLITE:
|
|
1323
|
+
url = f"sqlite:///{database}"
|
|
1324
|
+
elif dialect == DatabaseDialect.POSTGRESQL:
|
|
1325
|
+
url = f"postgresql://{username}:{password}@{host}:{port}/{database}"
|
|
1326
|
+
elif dialect == DatabaseDialect.MYSQL:
|
|
1327
|
+
url = f"mysql+pymysql://{username}:{password}@{host}:{port}/{database}"
|
|
1328
|
+
elif dialect == DatabaseDialect.MSSQL:
|
|
1329
|
+
url = f"mssql+pyodbc://{username}:{password}@{host}:{port}/{database}?driver=ODBC+Driver+17+for+SQL+Server"
|
|
1330
|
+
elif dialect == DatabaseDialect.ORACLE:
|
|
1331
|
+
url = f"oracle+cx_oracle://{username}:{password}@{host}:{port}/{database}"
|
|
1332
|
+
else:
|
|
1333
|
+
raise ValueError(f"Unsupported dialect: {dialect}")
|
|
1334
|
+
|
|
1335
|
+
return create_pool_manager(url, **kwargs)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
# =============================================================================
|
|
1339
|
+
# Exports
|
|
1340
|
+
# =============================================================================
|
|
1341
|
+
|
|
1342
|
+
__all__ = [
|
|
1343
|
+
# Enums
|
|
1344
|
+
"PoolStrategy",
|
|
1345
|
+
"DatabaseDialect",
|
|
1346
|
+
"ConnectionState",
|
|
1347
|
+
"CircuitState",
|
|
1348
|
+
# Configuration
|
|
1349
|
+
"PoolConfig",
|
|
1350
|
+
"RetryConfig",
|
|
1351
|
+
"CircuitBreakerConfig",
|
|
1352
|
+
"HealthCheckConfig",
|
|
1353
|
+
"ConnectionPoolConfig",
|
|
1354
|
+
# Core classes
|
|
1355
|
+
"PoolMetrics",
|
|
1356
|
+
"CircuitBreaker",
|
|
1357
|
+
"RetryHandler",
|
|
1358
|
+
"ConnectionPoolManager",
|
|
1359
|
+
# Factory functions
|
|
1360
|
+
"create_pool_manager",
|
|
1361
|
+
"create_pool_for_dialect",
|
|
1362
|
+
# Protocols
|
|
1363
|
+
"PoolMetricsProtocol",
|
|
1364
|
+
"ConnectionPoolProtocol",
|
|
1365
|
+
]
|