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,1180 @@
|
|
|
1
|
+
"""Resilience patterns for cache backends.
|
|
2
|
+
|
|
3
|
+
This module provides fault-tolerant caching with:
|
|
4
|
+
- Circuit breaker pattern for failure detection
|
|
5
|
+
- Automatic fallback to alternative backends
|
|
6
|
+
- Retry logic with exponential backoff
|
|
7
|
+
- Health monitoring and auto-recovery
|
|
8
|
+
- Comprehensive logging and observability
|
|
9
|
+
|
|
10
|
+
Key classes:
|
|
11
|
+
- CircuitBreaker: Prevents cascade failures
|
|
12
|
+
- RetryPolicy: Configurable retry with backoff
|
|
13
|
+
- ResilientCacheBackend: Wrapper with resilience patterns
|
|
14
|
+
- FallbackChain: Multi-backend fallback chain
|
|
15
|
+
- HealthMonitor: Continuous backend health checking
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
from truthound.profiler.resilience import (
|
|
19
|
+
ResilientCacheBackend,
|
|
20
|
+
FallbackChain,
|
|
21
|
+
CircuitBreakerConfig,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Create resilient Redis with memory fallback
|
|
25
|
+
cache = ResilientCacheBackend(
|
|
26
|
+
primary=RedisCacheBackend(host="redis.example.com"),
|
|
27
|
+
fallback=MemoryCacheBackend(),
|
|
28
|
+
circuit_breaker=CircuitBreakerConfig(
|
|
29
|
+
failure_threshold=5,
|
|
30
|
+
recovery_timeout=30,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
import threading
|
|
39
|
+
import time
|
|
40
|
+
from abc import ABC, abstractmethod
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from datetime import datetime, timedelta
|
|
43
|
+
from enum import Enum
|
|
44
|
+
from functools import wraps
|
|
45
|
+
from typing import Any, Callable, Generic, TypeVar
|
|
46
|
+
|
|
47
|
+
from truthound.profiler.caching import (
|
|
48
|
+
CacheBackend,
|
|
49
|
+
CacheEntry,
|
|
50
|
+
MemoryCacheBackend,
|
|
51
|
+
FileCacheBackend,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Set up logging
|
|
56
|
+
logger = logging.getLogger("truthound.cache.resilience")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Types
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
T = TypeVar("T")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CircuitState(str, Enum):
|
|
67
|
+
"""Circuit breaker states."""
|
|
68
|
+
|
|
69
|
+
CLOSED = "closed" # Normal operation
|
|
70
|
+
OPEN = "open" # Failing, reject requests
|
|
71
|
+
HALF_OPEN = "half_open" # Testing recovery
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BackendHealth(str, Enum):
|
|
75
|
+
"""Backend health status."""
|
|
76
|
+
|
|
77
|
+
HEALTHY = "healthy"
|
|
78
|
+
DEGRADED = "degraded"
|
|
79
|
+
UNHEALTHY = "unhealthy"
|
|
80
|
+
UNKNOWN = "unknown"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FailureType(str, Enum):
|
|
84
|
+
"""Types of failures for classification."""
|
|
85
|
+
|
|
86
|
+
CONNECTION = "connection"
|
|
87
|
+
TIMEOUT = "timeout"
|
|
88
|
+
SERIALIZATION = "serialization"
|
|
89
|
+
UNKNOWN = "unknown"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Configuration
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class CircuitBreakerConfig:
|
|
99
|
+
"""Configuration for circuit breaker.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
failure_threshold: Number of failures before opening circuit
|
|
103
|
+
success_threshold: Number of successes to close circuit
|
|
104
|
+
recovery_timeout: Seconds to wait before trying half-open
|
|
105
|
+
failure_window: Window in seconds to count failures
|
|
106
|
+
excluded_exceptions: Exceptions that don't count as failures
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
failure_threshold: int = 5
|
|
110
|
+
success_threshold: int = 2
|
|
111
|
+
recovery_timeout: float = 30.0
|
|
112
|
+
failure_window: float = 60.0
|
|
113
|
+
excluded_exceptions: tuple[type[Exception], ...] = ()
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def aggressive(cls) -> "CircuitBreakerConfig":
|
|
117
|
+
"""Aggressive config - opens quickly, recovers slowly."""
|
|
118
|
+
return cls(
|
|
119
|
+
failure_threshold=3,
|
|
120
|
+
success_threshold=3,
|
|
121
|
+
recovery_timeout=60.0,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def lenient(cls) -> "CircuitBreakerConfig":
|
|
126
|
+
"""Lenient config - tolerates more failures."""
|
|
127
|
+
return cls(
|
|
128
|
+
failure_threshold=10,
|
|
129
|
+
success_threshold=1,
|
|
130
|
+
recovery_timeout=15.0,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def disabled(cls) -> "CircuitBreakerConfig":
|
|
135
|
+
"""Effectively disabled circuit breaker."""
|
|
136
|
+
return cls(
|
|
137
|
+
failure_threshold=1000000,
|
|
138
|
+
recovery_timeout=0.1,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class RetryConfig:
|
|
144
|
+
"""Configuration for retry logic.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
max_attempts: Maximum retry attempts (1 = no retry)
|
|
148
|
+
base_delay: Base delay in seconds
|
|
149
|
+
max_delay: Maximum delay cap
|
|
150
|
+
exponential_base: Multiplier for exponential backoff
|
|
151
|
+
jitter: Add random jitter to delays
|
|
152
|
+
retryable_exceptions: Exceptions that trigger retry
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
max_attempts: int = 3
|
|
156
|
+
base_delay: float = 0.1
|
|
157
|
+
max_delay: float = 10.0
|
|
158
|
+
exponential_base: float = 2.0
|
|
159
|
+
jitter: bool = True
|
|
160
|
+
retryable_exceptions: tuple[type[Exception], ...] = (
|
|
161
|
+
ConnectionError,
|
|
162
|
+
TimeoutError,
|
|
163
|
+
OSError,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
167
|
+
"""Calculate delay for given attempt number."""
|
|
168
|
+
delay = self.base_delay * (self.exponential_base ** attempt)
|
|
169
|
+
delay = min(delay, self.max_delay)
|
|
170
|
+
|
|
171
|
+
if self.jitter:
|
|
172
|
+
import random
|
|
173
|
+
delay *= (0.5 + random.random())
|
|
174
|
+
|
|
175
|
+
return delay
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def no_retry(cls) -> "RetryConfig":
|
|
179
|
+
"""No retry configuration."""
|
|
180
|
+
return cls(max_attempts=1)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def quick(cls) -> "RetryConfig":
|
|
184
|
+
"""Quick retry for transient failures."""
|
|
185
|
+
return cls(
|
|
186
|
+
max_attempts=3,
|
|
187
|
+
base_delay=0.05,
|
|
188
|
+
max_delay=1.0,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def persistent(cls) -> "RetryConfig":
|
|
193
|
+
"""Persistent retry for important operations."""
|
|
194
|
+
return cls(
|
|
195
|
+
max_attempts=5,
|
|
196
|
+
base_delay=0.5,
|
|
197
|
+
max_delay=30.0,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class HealthCheckConfig:
|
|
203
|
+
"""Configuration for health monitoring.
|
|
204
|
+
|
|
205
|
+
Attributes:
|
|
206
|
+
check_interval: Seconds between health checks
|
|
207
|
+
timeout: Timeout for health check operations
|
|
208
|
+
healthy_threshold: Consecutive successes to mark healthy
|
|
209
|
+
unhealthy_threshold: Consecutive failures to mark unhealthy
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
check_interval: float = 30.0
|
|
213
|
+
timeout: float = 5.0
|
|
214
|
+
healthy_threshold: int = 2
|
|
215
|
+
unhealthy_threshold: int = 3
|
|
216
|
+
enabled: bool = True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# =============================================================================
|
|
220
|
+
# Circuit Breaker
|
|
221
|
+
# =============================================================================
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class FailureRecord:
|
|
226
|
+
"""Record of a failure event."""
|
|
227
|
+
|
|
228
|
+
timestamp: datetime
|
|
229
|
+
exception_type: str
|
|
230
|
+
failure_type: FailureType
|
|
231
|
+
message: str
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CircuitBreaker:
|
|
235
|
+
"""Circuit breaker pattern implementation.
|
|
236
|
+
|
|
237
|
+
Prevents cascade failures by tracking failure rates and
|
|
238
|
+
temporarily disabling operations when failure threshold is reached.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
breaker = CircuitBreaker(CircuitBreakerConfig(failure_threshold=5))
|
|
242
|
+
|
|
243
|
+
@breaker.protect
|
|
244
|
+
def risky_operation():
|
|
245
|
+
return redis_client.get("key")
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(self, config: CircuitBreakerConfig | None = None):
|
|
249
|
+
self.config = config or CircuitBreakerConfig()
|
|
250
|
+
self._state = CircuitState.CLOSED
|
|
251
|
+
self._failures: list[FailureRecord] = []
|
|
252
|
+
self._successes_in_half_open = 0
|
|
253
|
+
self._last_failure_time: datetime | None = None
|
|
254
|
+
self._opened_at: datetime | None = None
|
|
255
|
+
self._lock = threading.RLock()
|
|
256
|
+
|
|
257
|
+
# Statistics
|
|
258
|
+
self._total_calls = 0
|
|
259
|
+
self._total_failures = 0
|
|
260
|
+
self._total_rejections = 0
|
|
261
|
+
self._state_changes: list[tuple[datetime, CircuitState, CircuitState]] = []
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def state(self) -> CircuitState:
|
|
265
|
+
"""Get current circuit state."""
|
|
266
|
+
with self._lock:
|
|
267
|
+
self._check_recovery()
|
|
268
|
+
return self._state
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def is_open(self) -> bool:
|
|
272
|
+
"""Check if circuit is open (rejecting requests)."""
|
|
273
|
+
return self.state == CircuitState.OPEN
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def is_closed(self) -> bool:
|
|
277
|
+
"""Check if circuit is closed (normal operation)."""
|
|
278
|
+
return self.state == CircuitState.CLOSED
|
|
279
|
+
|
|
280
|
+
def _check_recovery(self) -> None:
|
|
281
|
+
"""Check if circuit should transition to half-open."""
|
|
282
|
+
if self._state != CircuitState.OPEN:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
if self._opened_at is None:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
elapsed = (datetime.now() - self._opened_at).total_seconds()
|
|
289
|
+
if elapsed >= self.config.recovery_timeout:
|
|
290
|
+
self._transition_to(CircuitState.HALF_OPEN)
|
|
291
|
+
|
|
292
|
+
def _transition_to(self, new_state: CircuitState) -> None:
|
|
293
|
+
"""Transition to a new state."""
|
|
294
|
+
old_state = self._state
|
|
295
|
+
self._state = new_state
|
|
296
|
+
self._state_changes.append((datetime.now(), old_state, new_state))
|
|
297
|
+
|
|
298
|
+
logger.info(
|
|
299
|
+
f"Circuit breaker state change: {old_state.value} -> {new_state.value}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if new_state == CircuitState.OPEN:
|
|
303
|
+
self._opened_at = datetime.now()
|
|
304
|
+
elif new_state == CircuitState.CLOSED:
|
|
305
|
+
self._failures.clear()
|
|
306
|
+
self._successes_in_half_open = 0
|
|
307
|
+
|
|
308
|
+
def _count_recent_failures(self) -> int:
|
|
309
|
+
"""Count failures within the failure window."""
|
|
310
|
+
cutoff = datetime.now() - timedelta(seconds=self.config.failure_window)
|
|
311
|
+
return sum(1 for f in self._failures if f.timestamp > cutoff)
|
|
312
|
+
|
|
313
|
+
def _classify_failure(self, exc: Exception) -> FailureType:
|
|
314
|
+
"""Classify the type of failure."""
|
|
315
|
+
if isinstance(exc, (ConnectionError, ConnectionRefusedError)):
|
|
316
|
+
return FailureType.CONNECTION
|
|
317
|
+
elif isinstance(exc, TimeoutError):
|
|
318
|
+
return FailureType.TIMEOUT
|
|
319
|
+
elif isinstance(exc, (TypeError, ValueError)):
|
|
320
|
+
return FailureType.SERIALIZATION
|
|
321
|
+
else:
|
|
322
|
+
return FailureType.UNKNOWN
|
|
323
|
+
|
|
324
|
+
def record_success(self) -> None:
|
|
325
|
+
"""Record a successful operation."""
|
|
326
|
+
with self._lock:
|
|
327
|
+
self._total_calls += 1
|
|
328
|
+
|
|
329
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
330
|
+
self._successes_in_half_open += 1
|
|
331
|
+
if self._successes_in_half_open >= self.config.success_threshold:
|
|
332
|
+
self._transition_to(CircuitState.CLOSED)
|
|
333
|
+
|
|
334
|
+
def record_failure(self, exc: Exception) -> None:
|
|
335
|
+
"""Record a failed operation."""
|
|
336
|
+
with self._lock:
|
|
337
|
+
self._total_calls += 1
|
|
338
|
+
self._total_failures += 1
|
|
339
|
+
|
|
340
|
+
# Check if this exception should be excluded
|
|
341
|
+
if isinstance(exc, self.config.excluded_exceptions):
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
failure = FailureRecord(
|
|
345
|
+
timestamp=datetime.now(),
|
|
346
|
+
exception_type=type(exc).__name__,
|
|
347
|
+
failure_type=self._classify_failure(exc),
|
|
348
|
+
message=str(exc),
|
|
349
|
+
)
|
|
350
|
+
self._failures.append(failure)
|
|
351
|
+
self._last_failure_time = datetime.now()
|
|
352
|
+
|
|
353
|
+
# Check state transitions
|
|
354
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
355
|
+
self._transition_to(CircuitState.OPEN)
|
|
356
|
+
elif self._state == CircuitState.CLOSED:
|
|
357
|
+
if self._count_recent_failures() >= self.config.failure_threshold:
|
|
358
|
+
self._transition_to(CircuitState.OPEN)
|
|
359
|
+
|
|
360
|
+
def can_execute(self) -> bool:
|
|
361
|
+
"""Check if operation can be executed."""
|
|
362
|
+
with self._lock:
|
|
363
|
+
self._check_recovery()
|
|
364
|
+
|
|
365
|
+
if self._state == CircuitState.OPEN:
|
|
366
|
+
self._total_rejections += 1
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
def protect(self, func: Callable[..., T]) -> Callable[..., T]:
|
|
372
|
+
"""Decorator to protect a function with circuit breaker.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
func: Function to protect
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Wrapped function
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
CircuitOpenError: If circuit is open
|
|
382
|
+
"""
|
|
383
|
+
@wraps(func)
|
|
384
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
385
|
+
if not self.can_execute():
|
|
386
|
+
raise CircuitOpenError(
|
|
387
|
+
f"Circuit breaker is open. "
|
|
388
|
+
f"Recovery in {self.time_until_recovery:.1f}s"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
result = func(*args, **kwargs)
|
|
393
|
+
self.record_success()
|
|
394
|
+
return result
|
|
395
|
+
except Exception as e:
|
|
396
|
+
self.record_failure(e)
|
|
397
|
+
raise
|
|
398
|
+
|
|
399
|
+
return wrapper
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def time_until_recovery(self) -> float:
|
|
403
|
+
"""Time in seconds until circuit might recover."""
|
|
404
|
+
if self._state != CircuitState.OPEN or self._opened_at is None:
|
|
405
|
+
return 0.0
|
|
406
|
+
|
|
407
|
+
elapsed = (datetime.now() - self._opened_at).total_seconds()
|
|
408
|
+
remaining = self.config.recovery_timeout - elapsed
|
|
409
|
+
return max(0.0, remaining)
|
|
410
|
+
|
|
411
|
+
def reset(self) -> None:
|
|
412
|
+
"""Manually reset the circuit breaker."""
|
|
413
|
+
with self._lock:
|
|
414
|
+
self._transition_to(CircuitState.CLOSED)
|
|
415
|
+
self._failures.clear()
|
|
416
|
+
self._successes_in_half_open = 0
|
|
417
|
+
|
|
418
|
+
def get_stats(self) -> dict[str, Any]:
|
|
419
|
+
"""Get circuit breaker statistics."""
|
|
420
|
+
with self._lock:
|
|
421
|
+
return {
|
|
422
|
+
"state": self._state.value,
|
|
423
|
+
"total_calls": self._total_calls,
|
|
424
|
+
"total_failures": self._total_failures,
|
|
425
|
+
"total_rejections": self._total_rejections,
|
|
426
|
+
"recent_failures": self._count_recent_failures(),
|
|
427
|
+
"failure_threshold": self.config.failure_threshold,
|
|
428
|
+
"time_until_recovery": self.time_until_recovery,
|
|
429
|
+
"last_failure": self._last_failure_time.isoformat() if self._last_failure_time else None,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class CircuitOpenError(Exception):
|
|
434
|
+
"""Raised when circuit breaker is open."""
|
|
435
|
+
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# =============================================================================
|
|
440
|
+
# Retry Logic
|
|
441
|
+
# =============================================================================
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class RetryPolicy:
|
|
445
|
+
"""Retry policy with exponential backoff.
|
|
446
|
+
|
|
447
|
+
Example:
|
|
448
|
+
policy = RetryPolicy(RetryConfig(max_attempts=3))
|
|
449
|
+
|
|
450
|
+
@policy.retry
|
|
451
|
+
def flaky_operation():
|
|
452
|
+
return http_call()
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def __init__(self, config: RetryConfig | None = None):
|
|
456
|
+
self.config = config or RetryConfig()
|
|
457
|
+
self._total_attempts = 0
|
|
458
|
+
self._total_retries = 0
|
|
459
|
+
self._lock = threading.Lock()
|
|
460
|
+
|
|
461
|
+
def should_retry(self, exc: Exception, attempt: int) -> bool:
|
|
462
|
+
"""Determine if operation should be retried."""
|
|
463
|
+
if attempt >= self.config.max_attempts:
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
return isinstance(exc, self.config.retryable_exceptions)
|
|
467
|
+
|
|
468
|
+
def execute_with_retry(
|
|
469
|
+
self,
|
|
470
|
+
func: Callable[..., T],
|
|
471
|
+
*args: Any,
|
|
472
|
+
**kwargs: Any,
|
|
473
|
+
) -> T:
|
|
474
|
+
"""Execute function with retry logic.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
func: Function to execute
|
|
478
|
+
*args: Positional arguments
|
|
479
|
+
**kwargs: Keyword arguments
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Function result
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
Last exception if all retries fail
|
|
486
|
+
"""
|
|
487
|
+
last_exception: Exception | None = None
|
|
488
|
+
|
|
489
|
+
for attempt in range(self.config.max_attempts):
|
|
490
|
+
with self._lock:
|
|
491
|
+
self._total_attempts += 1
|
|
492
|
+
if attempt > 0:
|
|
493
|
+
self._total_retries += 1
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
return func(*args, **kwargs)
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
last_exception = e
|
|
500
|
+
|
|
501
|
+
if not self.should_retry(e, attempt + 1):
|
|
502
|
+
logger.debug(
|
|
503
|
+
f"Not retrying {func.__name__}: {type(e).__name__} "
|
|
504
|
+
f"is not retryable or max attempts reached"
|
|
505
|
+
)
|
|
506
|
+
raise
|
|
507
|
+
|
|
508
|
+
delay = self.config.calculate_delay(attempt)
|
|
509
|
+
logger.warning(
|
|
510
|
+
f"Retry {attempt + 1}/{self.config.max_attempts} for "
|
|
511
|
+
f"{func.__name__} after {delay:.2f}s: {e}"
|
|
512
|
+
)
|
|
513
|
+
time.sleep(delay)
|
|
514
|
+
|
|
515
|
+
if last_exception:
|
|
516
|
+
raise last_exception
|
|
517
|
+
|
|
518
|
+
raise RuntimeError("Unexpected state in retry logic")
|
|
519
|
+
|
|
520
|
+
def retry(self, func: Callable[..., T]) -> Callable[..., T]:
|
|
521
|
+
"""Decorator to add retry logic to a function."""
|
|
522
|
+
@wraps(func)
|
|
523
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
524
|
+
return self.execute_with_retry(func, *args, **kwargs)
|
|
525
|
+
|
|
526
|
+
return wrapper
|
|
527
|
+
|
|
528
|
+
def get_stats(self) -> dict[str, Any]:
|
|
529
|
+
"""Get retry statistics."""
|
|
530
|
+
with self._lock:
|
|
531
|
+
return {
|
|
532
|
+
"total_attempts": self._total_attempts,
|
|
533
|
+
"total_retries": self._total_retries,
|
|
534
|
+
"retry_rate": (
|
|
535
|
+
self._total_retries / self._total_attempts
|
|
536
|
+
if self._total_attempts > 0 else 0.0
|
|
537
|
+
),
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# =============================================================================
|
|
542
|
+
# Health Monitor
|
|
543
|
+
# =============================================================================
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class HealthMonitor:
|
|
547
|
+
"""Monitors backend health with periodic checks.
|
|
548
|
+
|
|
549
|
+
Example:
|
|
550
|
+
monitor = HealthMonitor(backend, HealthCheckConfig())
|
|
551
|
+
monitor.start()
|
|
552
|
+
|
|
553
|
+
if monitor.is_healthy:
|
|
554
|
+
backend.get(key)
|
|
555
|
+
|
|
556
|
+
monitor.stop()
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
def __init__(
|
|
560
|
+
self,
|
|
561
|
+
backend: CacheBackend,
|
|
562
|
+
config: HealthCheckConfig | None = None,
|
|
563
|
+
name: str = "cache",
|
|
564
|
+
):
|
|
565
|
+
self.backend = backend
|
|
566
|
+
self.config = config or HealthCheckConfig()
|
|
567
|
+
self.name = name
|
|
568
|
+
|
|
569
|
+
self._health = BackendHealth.UNKNOWN
|
|
570
|
+
self._consecutive_successes = 0
|
|
571
|
+
self._consecutive_failures = 0
|
|
572
|
+
self._last_check: datetime | None = None
|
|
573
|
+
self._last_check_duration: float = 0.0
|
|
574
|
+
self._running = False
|
|
575
|
+
self._thread: threading.Thread | None = None
|
|
576
|
+
self._lock = threading.RLock()
|
|
577
|
+
|
|
578
|
+
# Statistics
|
|
579
|
+
self._total_checks = 0
|
|
580
|
+
self._total_failures = 0
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def health(self) -> BackendHealth:
|
|
584
|
+
"""Get current health status."""
|
|
585
|
+
with self._lock:
|
|
586
|
+
return self._health
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def is_healthy(self) -> bool:
|
|
590
|
+
"""Check if backend is healthy."""
|
|
591
|
+
return self.health == BackendHealth.HEALTHY
|
|
592
|
+
|
|
593
|
+
def check_health(self) -> bool:
|
|
594
|
+
"""Perform a health check.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
True if healthy
|
|
598
|
+
"""
|
|
599
|
+
start = time.perf_counter()
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
# Try a simple operation
|
|
603
|
+
test_key = f"__health_check_{self.name}__"
|
|
604
|
+
self.backend.exists(test_key)
|
|
605
|
+
|
|
606
|
+
with self._lock:
|
|
607
|
+
self._total_checks += 1
|
|
608
|
+
self._consecutive_successes += 1
|
|
609
|
+
self._consecutive_failures = 0
|
|
610
|
+
self._last_check = datetime.now()
|
|
611
|
+
self._last_check_duration = time.perf_counter() - start
|
|
612
|
+
|
|
613
|
+
if self._consecutive_successes >= self.config.healthy_threshold:
|
|
614
|
+
self._health = BackendHealth.HEALTHY
|
|
615
|
+
elif self._health == BackendHealth.UNHEALTHY:
|
|
616
|
+
self._health = BackendHealth.DEGRADED
|
|
617
|
+
|
|
618
|
+
logger.debug(
|
|
619
|
+
f"Health check passed for {self.name}: "
|
|
620
|
+
f"{self._last_check_duration*1000:.1f}ms"
|
|
621
|
+
)
|
|
622
|
+
return True
|
|
623
|
+
|
|
624
|
+
except Exception as e:
|
|
625
|
+
with self._lock:
|
|
626
|
+
self._total_checks += 1
|
|
627
|
+
self._total_failures += 1
|
|
628
|
+
self._consecutive_failures += 1
|
|
629
|
+
self._consecutive_successes = 0
|
|
630
|
+
self._last_check = datetime.now()
|
|
631
|
+
self._last_check_duration = time.perf_counter() - start
|
|
632
|
+
|
|
633
|
+
if self._consecutive_failures >= self.config.unhealthy_threshold:
|
|
634
|
+
self._health = BackendHealth.UNHEALTHY
|
|
635
|
+
else:
|
|
636
|
+
self._health = BackendHealth.DEGRADED
|
|
637
|
+
|
|
638
|
+
logger.warning(f"Health check failed for {self.name}: {e}")
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
def _monitor_loop(self) -> None:
|
|
642
|
+
"""Background monitoring loop."""
|
|
643
|
+
while self._running:
|
|
644
|
+
try:
|
|
645
|
+
self.check_health()
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.error(f"Health monitor error: {e}")
|
|
648
|
+
|
|
649
|
+
time.sleep(self.config.check_interval)
|
|
650
|
+
|
|
651
|
+
def start(self) -> None:
|
|
652
|
+
"""Start background health monitoring."""
|
|
653
|
+
if not self.config.enabled:
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
if self._running:
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
self._running = True
|
|
660
|
+
self._thread = threading.Thread(
|
|
661
|
+
target=self._monitor_loop,
|
|
662
|
+
daemon=True,
|
|
663
|
+
name=f"health-monitor-{self.name}",
|
|
664
|
+
)
|
|
665
|
+
self._thread.start()
|
|
666
|
+
logger.info(f"Started health monitor for {self.name}")
|
|
667
|
+
|
|
668
|
+
def stop(self) -> None:
|
|
669
|
+
"""Stop background health monitoring."""
|
|
670
|
+
self._running = False
|
|
671
|
+
if self._thread:
|
|
672
|
+
self._thread.join(timeout=5.0)
|
|
673
|
+
self._thread = None
|
|
674
|
+
logger.info(f"Stopped health monitor for {self.name}")
|
|
675
|
+
|
|
676
|
+
def get_stats(self) -> dict[str, Any]:
|
|
677
|
+
"""Get health monitor statistics."""
|
|
678
|
+
with self._lock:
|
|
679
|
+
return {
|
|
680
|
+
"name": self.name,
|
|
681
|
+
"health": self._health.value,
|
|
682
|
+
"is_healthy": self.is_healthy,
|
|
683
|
+
"consecutive_successes": self._consecutive_successes,
|
|
684
|
+
"consecutive_failures": self._consecutive_failures,
|
|
685
|
+
"total_checks": self._total_checks,
|
|
686
|
+
"total_failures": self._total_failures,
|
|
687
|
+
"last_check": self._last_check.isoformat() if self._last_check else None,
|
|
688
|
+
"last_check_duration_ms": self._last_check_duration * 1000,
|
|
689
|
+
"running": self._running,
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
# =============================================================================
|
|
694
|
+
# Resilient Cache Backend
|
|
695
|
+
# =============================================================================
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@dataclass
|
|
699
|
+
class ResilienceConfig:
|
|
700
|
+
"""Configuration for resilient cache backend."""
|
|
701
|
+
|
|
702
|
+
circuit_breaker: CircuitBreakerConfig = field(
|
|
703
|
+
default_factory=CircuitBreakerConfig
|
|
704
|
+
)
|
|
705
|
+
retry: RetryConfig = field(default_factory=RetryConfig)
|
|
706
|
+
health_check: HealthCheckConfig = field(default_factory=HealthCheckConfig)
|
|
707
|
+
fallback_on_error: bool = True
|
|
708
|
+
log_failures: bool = True
|
|
709
|
+
|
|
710
|
+
@classmethod
|
|
711
|
+
def default(cls) -> "ResilienceConfig":
|
|
712
|
+
"""Default resilience configuration."""
|
|
713
|
+
return cls()
|
|
714
|
+
|
|
715
|
+
@classmethod
|
|
716
|
+
def high_availability(cls) -> "ResilienceConfig":
|
|
717
|
+
"""High availability configuration."""
|
|
718
|
+
return cls(
|
|
719
|
+
circuit_breaker=CircuitBreakerConfig.lenient(),
|
|
720
|
+
retry=RetryConfig.persistent(),
|
|
721
|
+
health_check=HealthCheckConfig(
|
|
722
|
+
check_interval=10.0,
|
|
723
|
+
healthy_threshold=1,
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
@classmethod
|
|
728
|
+
def low_latency(cls) -> "ResilienceConfig":
|
|
729
|
+
"""Low latency configuration - fail fast."""
|
|
730
|
+
return cls(
|
|
731
|
+
circuit_breaker=CircuitBreakerConfig.aggressive(),
|
|
732
|
+
retry=RetryConfig.no_retry(),
|
|
733
|
+
health_check=HealthCheckConfig(check_interval=60.0),
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class ResilientCacheBackend(CacheBackend):
|
|
738
|
+
"""Cache backend wrapper with resilience patterns.
|
|
739
|
+
|
|
740
|
+
Wraps a primary backend with circuit breaker, retry logic,
|
|
741
|
+
and optional fallback to a secondary backend.
|
|
742
|
+
|
|
743
|
+
Example:
|
|
744
|
+
primary = RedisCacheBackend(host="redis.example.com")
|
|
745
|
+
fallback = MemoryCacheBackend()
|
|
746
|
+
|
|
747
|
+
cache = ResilientCacheBackend(
|
|
748
|
+
primary=primary,
|
|
749
|
+
fallback=fallback,
|
|
750
|
+
config=ResilienceConfig.high_availability(),
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
# Automatically falls back to memory on Redis failure
|
|
754
|
+
entry = cache.get("my-key")
|
|
755
|
+
"""
|
|
756
|
+
|
|
757
|
+
def __init__(
|
|
758
|
+
self,
|
|
759
|
+
primary: CacheBackend,
|
|
760
|
+
fallback: CacheBackend | None = None,
|
|
761
|
+
config: ResilienceConfig | None = None,
|
|
762
|
+
name: str = "resilient-cache",
|
|
763
|
+
):
|
|
764
|
+
self.primary = primary
|
|
765
|
+
self.fallback = fallback or MemoryCacheBackend()
|
|
766
|
+
self.config = config or ResilienceConfig.default()
|
|
767
|
+
self.name = name
|
|
768
|
+
|
|
769
|
+
# Initialize components
|
|
770
|
+
self._circuit_breaker = CircuitBreaker(self.config.circuit_breaker)
|
|
771
|
+
self._retry_policy = RetryPolicy(self.config.retry)
|
|
772
|
+
self._health_monitor = HealthMonitor(
|
|
773
|
+
primary,
|
|
774
|
+
self.config.health_check,
|
|
775
|
+
name=f"{name}-primary",
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# Statistics
|
|
779
|
+
self._primary_calls = 0
|
|
780
|
+
self._fallback_calls = 0
|
|
781
|
+
self._lock = threading.Lock()
|
|
782
|
+
|
|
783
|
+
# Start health monitoring
|
|
784
|
+
self._health_monitor.start()
|
|
785
|
+
|
|
786
|
+
def _execute_with_resilience(
|
|
787
|
+
self,
|
|
788
|
+
primary_fn: Callable[[], T],
|
|
789
|
+
fallback_fn: Callable[[], T] | None = None,
|
|
790
|
+
operation_name: str = "operation",
|
|
791
|
+
) -> T:
|
|
792
|
+
"""Execute an operation with full resilience patterns.
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
primary_fn: Primary operation
|
|
796
|
+
fallback_fn: Fallback operation
|
|
797
|
+
operation_name: Name for logging
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
Operation result
|
|
801
|
+
"""
|
|
802
|
+
# Check circuit breaker
|
|
803
|
+
if not self._circuit_breaker.can_execute():
|
|
804
|
+
if fallback_fn and self.config.fallback_on_error:
|
|
805
|
+
logger.debug(f"Circuit open, using fallback for {operation_name}")
|
|
806
|
+
with self._lock:
|
|
807
|
+
self._fallback_calls += 1
|
|
808
|
+
return fallback_fn()
|
|
809
|
+
raise CircuitOpenError(f"Circuit is open for {operation_name}")
|
|
810
|
+
|
|
811
|
+
# Try primary with retry
|
|
812
|
+
try:
|
|
813
|
+
result = self._retry_policy.execute_with_retry(primary_fn)
|
|
814
|
+
self._circuit_breaker.record_success()
|
|
815
|
+
with self._lock:
|
|
816
|
+
self._primary_calls += 1
|
|
817
|
+
return result
|
|
818
|
+
|
|
819
|
+
except Exception as e:
|
|
820
|
+
self._circuit_breaker.record_failure(e)
|
|
821
|
+
|
|
822
|
+
if self.config.log_failures:
|
|
823
|
+
logger.warning(
|
|
824
|
+
f"Primary cache failed for {operation_name}: {e}"
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# Try fallback
|
|
828
|
+
if fallback_fn and self.config.fallback_on_error:
|
|
829
|
+
logger.info(f"Using fallback for {operation_name}")
|
|
830
|
+
with self._lock:
|
|
831
|
+
self._fallback_calls += 1
|
|
832
|
+
return fallback_fn()
|
|
833
|
+
|
|
834
|
+
raise
|
|
835
|
+
|
|
836
|
+
def get(self, key: str) -> CacheEntry | None:
|
|
837
|
+
"""Get from cache with resilience."""
|
|
838
|
+
return self._execute_with_resilience(
|
|
839
|
+
primary_fn=lambda: self.primary.get(key),
|
|
840
|
+
fallback_fn=lambda: self.fallback.get(key),
|
|
841
|
+
operation_name=f"get:{key[:20]}",
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
def set(
|
|
845
|
+
self,
|
|
846
|
+
key: str,
|
|
847
|
+
entry: CacheEntry,
|
|
848
|
+
ttl: timedelta | None = None,
|
|
849
|
+
) -> None:
|
|
850
|
+
"""Set in cache with resilience."""
|
|
851
|
+
def primary_set() -> None:
|
|
852
|
+
self.primary.set(key, entry, ttl)
|
|
853
|
+
|
|
854
|
+
def fallback_set() -> None:
|
|
855
|
+
self.fallback.set(key, entry, ttl)
|
|
856
|
+
|
|
857
|
+
self._execute_with_resilience(
|
|
858
|
+
primary_fn=primary_set,
|
|
859
|
+
fallback_fn=fallback_set,
|
|
860
|
+
operation_name=f"set:{key[:20]}",
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
def delete(self, key: str) -> bool:
|
|
864
|
+
"""Delete from cache with resilience."""
|
|
865
|
+
try:
|
|
866
|
+
result = self._execute_with_resilience(
|
|
867
|
+
primary_fn=lambda: self.primary.delete(key),
|
|
868
|
+
fallback_fn=lambda: self.fallback.delete(key),
|
|
869
|
+
operation_name=f"delete:{key[:20]}",
|
|
870
|
+
)
|
|
871
|
+
return result
|
|
872
|
+
except Exception:
|
|
873
|
+
return False
|
|
874
|
+
|
|
875
|
+
def clear(self) -> int:
|
|
876
|
+
"""Clear cache."""
|
|
877
|
+
count = 0
|
|
878
|
+
try:
|
|
879
|
+
count += self.primary.clear()
|
|
880
|
+
except Exception as e:
|
|
881
|
+
logger.warning(f"Failed to clear primary cache: {e}")
|
|
882
|
+
|
|
883
|
+
try:
|
|
884
|
+
count += self.fallback.clear()
|
|
885
|
+
except Exception as e:
|
|
886
|
+
logger.warning(f"Failed to clear fallback cache: {e}")
|
|
887
|
+
|
|
888
|
+
return count
|
|
889
|
+
|
|
890
|
+
def exists(self, key: str) -> bool:
|
|
891
|
+
"""Check if key exists."""
|
|
892
|
+
return self._execute_with_resilience(
|
|
893
|
+
primary_fn=lambda: self.primary.exists(key),
|
|
894
|
+
fallback_fn=lambda: self.fallback.exists(key),
|
|
895
|
+
operation_name=f"exists:{key[:20]}",
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
def get_stats(self) -> dict[str, Any]:
|
|
899
|
+
"""Get comprehensive statistics."""
|
|
900
|
+
with self._lock:
|
|
901
|
+
total_calls = self._primary_calls + self._fallback_calls
|
|
902
|
+
return {
|
|
903
|
+
"type": "resilient",
|
|
904
|
+
"name": self.name,
|
|
905
|
+
"primary_calls": self._primary_calls,
|
|
906
|
+
"fallback_calls": self._fallback_calls,
|
|
907
|
+
"fallback_rate": (
|
|
908
|
+
self._fallback_calls / total_calls
|
|
909
|
+
if total_calls > 0 else 0.0
|
|
910
|
+
),
|
|
911
|
+
"circuit_breaker": self._circuit_breaker.get_stats(),
|
|
912
|
+
"retry": self._retry_policy.get_stats(),
|
|
913
|
+
"health": self._health_monitor.get_stats(),
|
|
914
|
+
"primary": self.primary.get_stats(),
|
|
915
|
+
"fallback": self.fallback.get_stats(),
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
def is_primary_healthy(self) -> bool:
|
|
919
|
+
"""Check if primary backend is healthy."""
|
|
920
|
+
return self._health_monitor.is_healthy
|
|
921
|
+
|
|
922
|
+
def force_primary_check(self) -> bool:
|
|
923
|
+
"""Force an immediate health check on primary."""
|
|
924
|
+
return self._health_monitor.check_health()
|
|
925
|
+
|
|
926
|
+
def reset_circuit(self) -> None:
|
|
927
|
+
"""Manually reset the circuit breaker."""
|
|
928
|
+
self._circuit_breaker.reset()
|
|
929
|
+
logger.info(f"Circuit breaker reset for {self.name}")
|
|
930
|
+
|
|
931
|
+
def shutdown(self) -> None:
|
|
932
|
+
"""Shutdown the resilient backend."""
|
|
933
|
+
self._health_monitor.stop()
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
# =============================================================================
|
|
937
|
+
# Fallback Chain
|
|
938
|
+
# =============================================================================
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
class FallbackChain(CacheBackend):
|
|
942
|
+
"""Chain of cache backends with automatic fallback.
|
|
943
|
+
|
|
944
|
+
Tries backends in order until one succeeds.
|
|
945
|
+
|
|
946
|
+
Example:
|
|
947
|
+
chain = FallbackChain([
|
|
948
|
+
RedisCacheBackend(host="primary-redis"),
|
|
949
|
+
RedisCacheBackend(host="secondary-redis"),
|
|
950
|
+
FileCacheBackend(cache_dir=".cache"),
|
|
951
|
+
MemoryCacheBackend(),
|
|
952
|
+
])
|
|
953
|
+
|
|
954
|
+
# Will try each backend in order until success
|
|
955
|
+
entry = chain.get("key")
|
|
956
|
+
"""
|
|
957
|
+
|
|
958
|
+
def __init__(
|
|
959
|
+
self,
|
|
960
|
+
backends: list[CacheBackend],
|
|
961
|
+
retry_config: RetryConfig | None = None,
|
|
962
|
+
):
|
|
963
|
+
if not backends:
|
|
964
|
+
raise ValueError("At least one backend is required")
|
|
965
|
+
|
|
966
|
+
self.backends = backends
|
|
967
|
+
self._retry_policy = RetryPolicy(retry_config or RetryConfig.quick())
|
|
968
|
+
self._lock = threading.Lock()
|
|
969
|
+
|
|
970
|
+
# Track which backends are working
|
|
971
|
+
self._backend_health: dict[int, bool] = {
|
|
972
|
+
i: True for i in range(len(backends))
|
|
973
|
+
}
|
|
974
|
+
self._calls_per_backend: dict[int, int] = {
|
|
975
|
+
i: 0 for i in range(len(backends))
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
def _try_backends(
|
|
979
|
+
self,
|
|
980
|
+
operation: Callable[[CacheBackend], T],
|
|
981
|
+
operation_name: str = "operation",
|
|
982
|
+
) -> T:
|
|
983
|
+
"""Try operation on backends in order."""
|
|
984
|
+
last_exception: Exception | None = None
|
|
985
|
+
|
|
986
|
+
for i, backend in enumerate(self.backends):
|
|
987
|
+
# Skip unhealthy backends (but try last one regardless)
|
|
988
|
+
if not self._backend_health[i] and i < len(self.backends) - 1:
|
|
989
|
+
continue
|
|
990
|
+
|
|
991
|
+
try:
|
|
992
|
+
result = operation(backend)
|
|
993
|
+
|
|
994
|
+
with self._lock:
|
|
995
|
+
self._calls_per_backend[i] += 1
|
|
996
|
+
self._backend_health[i] = True
|
|
997
|
+
|
|
998
|
+
if i > 0:
|
|
999
|
+
logger.debug(
|
|
1000
|
+
f"Fallback to backend {i} succeeded for {operation_name}"
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
return result
|
|
1004
|
+
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
last_exception = e
|
|
1007
|
+
with self._lock:
|
|
1008
|
+
self._backend_health[i] = False
|
|
1009
|
+
|
|
1010
|
+
logger.warning(
|
|
1011
|
+
f"Backend {i} failed for {operation_name}: {e}"
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
if last_exception:
|
|
1015
|
+
raise last_exception
|
|
1016
|
+
|
|
1017
|
+
raise RuntimeError("No backends available")
|
|
1018
|
+
|
|
1019
|
+
def get(self, key: str) -> CacheEntry | None:
|
|
1020
|
+
try:
|
|
1021
|
+
return self._try_backends(
|
|
1022
|
+
lambda b: b.get(key),
|
|
1023
|
+
f"get:{key[:20]}",
|
|
1024
|
+
)
|
|
1025
|
+
except Exception:
|
|
1026
|
+
return None
|
|
1027
|
+
|
|
1028
|
+
def set(
|
|
1029
|
+
self,
|
|
1030
|
+
key: str,
|
|
1031
|
+
entry: CacheEntry,
|
|
1032
|
+
ttl: timedelta | None = None,
|
|
1033
|
+
) -> None:
|
|
1034
|
+
self._try_backends(
|
|
1035
|
+
lambda b: b.set(key, entry, ttl),
|
|
1036
|
+
f"set:{key[:20]}",
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
def delete(self, key: str) -> bool:
|
|
1040
|
+
try:
|
|
1041
|
+
return self._try_backends(
|
|
1042
|
+
lambda b: b.delete(key),
|
|
1043
|
+
f"delete:{key[:20]}",
|
|
1044
|
+
)
|
|
1045
|
+
except Exception:
|
|
1046
|
+
return False
|
|
1047
|
+
|
|
1048
|
+
def clear(self) -> int:
|
|
1049
|
+
total = 0
|
|
1050
|
+
for backend in self.backends:
|
|
1051
|
+
try:
|
|
1052
|
+
total += backend.clear()
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
logger.warning(f"Failed to clear backend: {e}")
|
|
1055
|
+
return total
|
|
1056
|
+
|
|
1057
|
+
def exists(self, key: str) -> bool:
|
|
1058
|
+
try:
|
|
1059
|
+
return self._try_backends(
|
|
1060
|
+
lambda b: b.exists(key),
|
|
1061
|
+
f"exists:{key[:20]}",
|
|
1062
|
+
)
|
|
1063
|
+
except Exception:
|
|
1064
|
+
return False
|
|
1065
|
+
|
|
1066
|
+
def get_stats(self) -> dict[str, Any]:
|
|
1067
|
+
with self._lock:
|
|
1068
|
+
return {
|
|
1069
|
+
"type": "fallback_chain",
|
|
1070
|
+
"backend_count": len(self.backends),
|
|
1071
|
+
"backend_health": dict(self._backend_health),
|
|
1072
|
+
"calls_per_backend": dict(self._calls_per_backend),
|
|
1073
|
+
"backends": [b.get_stats() for b in self.backends],
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
# =============================================================================
|
|
1078
|
+
# Factory Functions
|
|
1079
|
+
# =============================================================================
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def create_resilient_redis_backend(
|
|
1083
|
+
host: str = "localhost",
|
|
1084
|
+
port: int = 6379,
|
|
1085
|
+
db: int = 0,
|
|
1086
|
+
password: str | None = None,
|
|
1087
|
+
fallback_to_memory: bool = True,
|
|
1088
|
+
fallback_to_file: bool = False,
|
|
1089
|
+
file_cache_dir: str = ".truthound_cache",
|
|
1090
|
+
config: ResilienceConfig | None = None,
|
|
1091
|
+
**redis_kwargs: Any,
|
|
1092
|
+
) -> CacheBackend:
|
|
1093
|
+
"""Create a resilient Redis backend with fallback.
|
|
1094
|
+
|
|
1095
|
+
This is the recommended way to create a Redis-backed cache
|
|
1096
|
+
with automatic fallback.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
host: Redis host
|
|
1100
|
+
port: Redis port
|
|
1101
|
+
db: Redis database
|
|
1102
|
+
password: Redis password
|
|
1103
|
+
fallback_to_memory: Use memory as fallback
|
|
1104
|
+
fallback_to_file: Use file as fallback
|
|
1105
|
+
file_cache_dir: Directory for file cache
|
|
1106
|
+
config: Resilience configuration
|
|
1107
|
+
**redis_kwargs: Additional Redis options
|
|
1108
|
+
|
|
1109
|
+
Returns:
|
|
1110
|
+
Configured resilient cache backend
|
|
1111
|
+
"""
|
|
1112
|
+
try:
|
|
1113
|
+
from truthound.profiler.caching import RedisCacheBackend
|
|
1114
|
+
primary = RedisCacheBackend(
|
|
1115
|
+
host=host,
|
|
1116
|
+
port=port,
|
|
1117
|
+
db=db,
|
|
1118
|
+
password=password,
|
|
1119
|
+
**redis_kwargs,
|
|
1120
|
+
)
|
|
1121
|
+
except ImportError:
|
|
1122
|
+
logger.warning(
|
|
1123
|
+
"Redis package not installed. Using memory cache."
|
|
1124
|
+
)
|
|
1125
|
+
return MemoryCacheBackend()
|
|
1126
|
+
|
|
1127
|
+
# Create fallback chain
|
|
1128
|
+
fallbacks: list[CacheBackend] = []
|
|
1129
|
+
if fallback_to_file:
|
|
1130
|
+
fallbacks.append(FileCacheBackend(cache_dir=file_cache_dir))
|
|
1131
|
+
if fallback_to_memory:
|
|
1132
|
+
fallbacks.append(MemoryCacheBackend())
|
|
1133
|
+
|
|
1134
|
+
if not fallbacks:
|
|
1135
|
+
fallbacks.append(MemoryCacheBackend())
|
|
1136
|
+
|
|
1137
|
+
# Use first fallback as the direct fallback
|
|
1138
|
+
fallback = fallbacks[0] if len(fallbacks) == 1 else FallbackChain(fallbacks)
|
|
1139
|
+
|
|
1140
|
+
return ResilientCacheBackend(
|
|
1141
|
+
primary=primary,
|
|
1142
|
+
fallback=fallback,
|
|
1143
|
+
config=config or ResilienceConfig.default(),
|
|
1144
|
+
name="resilient-redis",
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def create_high_availability_cache(
|
|
1149
|
+
primary_host: str,
|
|
1150
|
+
secondary_host: str | None = None,
|
|
1151
|
+
port: int = 6379,
|
|
1152
|
+
**kwargs: Any,
|
|
1153
|
+
) -> CacheBackend:
|
|
1154
|
+
"""Create a high-availability cache with multiple Redis instances.
|
|
1155
|
+
|
|
1156
|
+
Args:
|
|
1157
|
+
primary_host: Primary Redis host
|
|
1158
|
+
secondary_host: Secondary Redis host (optional)
|
|
1159
|
+
port: Redis port
|
|
1160
|
+
**kwargs: Additional options
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
High-availability cache backend
|
|
1164
|
+
"""
|
|
1165
|
+
backends: list[CacheBackend] = []
|
|
1166
|
+
|
|
1167
|
+
try:
|
|
1168
|
+
from truthound.profiler.caching import RedisCacheBackend
|
|
1169
|
+
|
|
1170
|
+
backends.append(RedisCacheBackend(host=primary_host, port=port))
|
|
1171
|
+
|
|
1172
|
+
if secondary_host:
|
|
1173
|
+
backends.append(RedisCacheBackend(host=secondary_host, port=port))
|
|
1174
|
+
except ImportError:
|
|
1175
|
+
logger.warning("Redis package not installed.")
|
|
1176
|
+
|
|
1177
|
+
backends.append(FileCacheBackend())
|
|
1178
|
+
backends.append(MemoryCacheBackend())
|
|
1179
|
+
|
|
1180
|
+
return FallbackChain(backends)
|