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,1083 @@
|
|
|
1
|
+
"""Storage backends for audit logging.
|
|
2
|
+
|
|
3
|
+
This module provides various storage implementations for persisting
|
|
4
|
+
audit events:
|
|
5
|
+
- Memory: Fast, in-process storage for development/testing
|
|
6
|
+
- File: JSON/JSONL file-based storage
|
|
7
|
+
- SQLite: Local database storage
|
|
8
|
+
- SQL: Generic SQL database storage (PostgreSQL, MySQL, etc.)
|
|
9
|
+
- Elasticsearch: Full-text search capable storage
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import gzip
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sqlite3
|
|
18
|
+
import threading
|
|
19
|
+
from abc import ABC
|
|
20
|
+
from collections import deque
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Iterator
|
|
25
|
+
|
|
26
|
+
from truthound.audit.core import (
|
|
27
|
+
AuditEvent,
|
|
28
|
+
AuditEventType,
|
|
29
|
+
AuditOutcome,
|
|
30
|
+
AuditStorage,
|
|
31
|
+
AuditStorageError,
|
|
32
|
+
current_timestamp,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# Memory Storage
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MemoryAuditStorage(AuditStorage):
|
|
42
|
+
"""In-memory storage for audit events.
|
|
43
|
+
|
|
44
|
+
Suitable for development, testing, and short-lived processes.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> storage = MemoryAuditStorage(max_events=10000)
|
|
48
|
+
>>> storage.write(event)
|
|
49
|
+
>>> events = storage.query(limit=10)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
max_events: int = 10000,
|
|
55
|
+
auto_expire: bool = True,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize memory storage.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
max_events: Maximum events to keep.
|
|
61
|
+
auto_expire: Automatically remove oldest events when full.
|
|
62
|
+
"""
|
|
63
|
+
self._events: deque[AuditEvent] = deque(maxlen=max_events if auto_expire else None)
|
|
64
|
+
self._index: dict[str, AuditEvent] = {}
|
|
65
|
+
self._lock = threading.Lock()
|
|
66
|
+
self._max_events = max_events
|
|
67
|
+
self._auto_expire = auto_expire
|
|
68
|
+
|
|
69
|
+
def write(self, event: AuditEvent) -> None:
|
|
70
|
+
"""Write a single audit event."""
|
|
71
|
+
with self._lock:
|
|
72
|
+
# Remove from index if we're at capacity and auto-expiring
|
|
73
|
+
if self._auto_expire and len(self._events) >= self._max_events:
|
|
74
|
+
old_event = self._events[0]
|
|
75
|
+
self._index.pop(old_event.id, None)
|
|
76
|
+
|
|
77
|
+
self._events.append(event)
|
|
78
|
+
self._index[event.id] = event
|
|
79
|
+
|
|
80
|
+
def write_batch(self, events: list[AuditEvent]) -> None:
|
|
81
|
+
"""Write multiple audit events."""
|
|
82
|
+
with self._lock:
|
|
83
|
+
for event in events:
|
|
84
|
+
if self._auto_expire and len(self._events) >= self._max_events:
|
|
85
|
+
old_event = self._events[0]
|
|
86
|
+
self._index.pop(old_event.id, None)
|
|
87
|
+
|
|
88
|
+
self._events.append(event)
|
|
89
|
+
self._index[event.id] = event
|
|
90
|
+
|
|
91
|
+
def read(self, event_id: str) -> AuditEvent | None:
|
|
92
|
+
"""Read a single audit event by ID."""
|
|
93
|
+
with self._lock:
|
|
94
|
+
return self._index.get(event_id)
|
|
95
|
+
|
|
96
|
+
def query(
|
|
97
|
+
self,
|
|
98
|
+
start_time: datetime | None = None,
|
|
99
|
+
end_time: datetime | None = None,
|
|
100
|
+
event_types: list[AuditEventType] | None = None,
|
|
101
|
+
actor_id: str | None = None,
|
|
102
|
+
resource_id: str | None = None,
|
|
103
|
+
outcome: AuditOutcome | None = None,
|
|
104
|
+
limit: int = 100,
|
|
105
|
+
offset: int = 0,
|
|
106
|
+
) -> list[AuditEvent]:
|
|
107
|
+
"""Query audit events."""
|
|
108
|
+
with self._lock:
|
|
109
|
+
results = []
|
|
110
|
+
|
|
111
|
+
for event in reversed(self._events): # Most recent first
|
|
112
|
+
if self._matches_filters(
|
|
113
|
+
event,
|
|
114
|
+
start_time,
|
|
115
|
+
end_time,
|
|
116
|
+
event_types,
|
|
117
|
+
actor_id,
|
|
118
|
+
resource_id,
|
|
119
|
+
outcome,
|
|
120
|
+
):
|
|
121
|
+
results.append(event)
|
|
122
|
+
|
|
123
|
+
# Apply offset and limit
|
|
124
|
+
return results[offset : offset + limit]
|
|
125
|
+
|
|
126
|
+
def count(
|
|
127
|
+
self,
|
|
128
|
+
start_time: datetime | None = None,
|
|
129
|
+
end_time: datetime | None = None,
|
|
130
|
+
event_types: list[AuditEventType] | None = None,
|
|
131
|
+
) -> int:
|
|
132
|
+
"""Count matching audit events."""
|
|
133
|
+
with self._lock:
|
|
134
|
+
count = 0
|
|
135
|
+
for event in self._events:
|
|
136
|
+
if self._matches_filters(
|
|
137
|
+
event,
|
|
138
|
+
start_time,
|
|
139
|
+
end_time,
|
|
140
|
+
event_types,
|
|
141
|
+
None,
|
|
142
|
+
None,
|
|
143
|
+
None,
|
|
144
|
+
):
|
|
145
|
+
count += 1
|
|
146
|
+
return count
|
|
147
|
+
|
|
148
|
+
def delete_before(self, before: datetime) -> int:
|
|
149
|
+
"""Delete events before a given time."""
|
|
150
|
+
with self._lock:
|
|
151
|
+
deleted = 0
|
|
152
|
+
events_to_keep = []
|
|
153
|
+
|
|
154
|
+
for event in self._events:
|
|
155
|
+
if event.timestamp >= before:
|
|
156
|
+
events_to_keep.append(event)
|
|
157
|
+
else:
|
|
158
|
+
self._index.pop(event.id, None)
|
|
159
|
+
deleted += 1
|
|
160
|
+
|
|
161
|
+
self._events.clear()
|
|
162
|
+
for event in events_to_keep:
|
|
163
|
+
self._events.append(event)
|
|
164
|
+
|
|
165
|
+
return deleted
|
|
166
|
+
|
|
167
|
+
def clear(self) -> None:
|
|
168
|
+
"""Clear all events."""
|
|
169
|
+
with self._lock:
|
|
170
|
+
self._events.clear()
|
|
171
|
+
self._index.clear()
|
|
172
|
+
|
|
173
|
+
def _matches_filters(
|
|
174
|
+
self,
|
|
175
|
+
event: AuditEvent,
|
|
176
|
+
start_time: datetime | None,
|
|
177
|
+
end_time: datetime | None,
|
|
178
|
+
event_types: list[AuditEventType] | None,
|
|
179
|
+
actor_id: str | None,
|
|
180
|
+
resource_id: str | None,
|
|
181
|
+
outcome: AuditOutcome | None,
|
|
182
|
+
) -> bool:
|
|
183
|
+
"""Check if event matches all filters."""
|
|
184
|
+
if start_time and event.timestamp < start_time:
|
|
185
|
+
return False
|
|
186
|
+
if end_time and event.timestamp > end_time:
|
|
187
|
+
return False
|
|
188
|
+
if event_types and event.event_type not in event_types:
|
|
189
|
+
return False
|
|
190
|
+
if actor_id and (not event.actor or event.actor.id != actor_id):
|
|
191
|
+
return False
|
|
192
|
+
if resource_id and (not event.resource or event.resource.id != resource_id):
|
|
193
|
+
return False
|
|
194
|
+
if outcome and event.outcome != outcome:
|
|
195
|
+
return False
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# =============================================================================
|
|
200
|
+
# File Storage
|
|
201
|
+
# =============================================================================
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class FileStorageConfig:
|
|
206
|
+
"""Configuration for file-based audit storage."""
|
|
207
|
+
|
|
208
|
+
path: str = "./audit_logs"
|
|
209
|
+
filename_pattern: str = "audit_{date}.jsonl"
|
|
210
|
+
compress: bool = False
|
|
211
|
+
max_file_size_mb: int = 100
|
|
212
|
+
rotate_daily: bool = True
|
|
213
|
+
encoding: str = "utf-8"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class FileAuditStorage(AuditStorage):
|
|
217
|
+
"""File-based storage for audit events.
|
|
218
|
+
|
|
219
|
+
Supports JSONL format with optional compression and rotation.
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> storage = FileAuditStorage(
|
|
223
|
+
... config=FileStorageConfig(
|
|
224
|
+
... path="./audit_logs",
|
|
225
|
+
... compress=True,
|
|
226
|
+
... )
|
|
227
|
+
... )
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(self, config: FileStorageConfig | None = None) -> None:
|
|
231
|
+
"""Initialize file storage.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
config: Storage configuration.
|
|
235
|
+
"""
|
|
236
|
+
self._config = config or FileStorageConfig()
|
|
237
|
+
self._lock = threading.Lock()
|
|
238
|
+
self._current_file: Any = None
|
|
239
|
+
self._current_filename: str = ""
|
|
240
|
+
|
|
241
|
+
# Ensure directory exists
|
|
242
|
+
Path(self._config.path).mkdir(parents=True, exist_ok=True)
|
|
243
|
+
|
|
244
|
+
def write(self, event: AuditEvent) -> None:
|
|
245
|
+
"""Write a single audit event."""
|
|
246
|
+
with self._lock:
|
|
247
|
+
self._ensure_file_open()
|
|
248
|
+
line = json.dumps(event.to_dict()) + "\n"
|
|
249
|
+
self._current_file.write(line)
|
|
250
|
+
self._current_file.flush()
|
|
251
|
+
|
|
252
|
+
def write_batch(self, events: list[AuditEvent]) -> None:
|
|
253
|
+
"""Write multiple audit events."""
|
|
254
|
+
with self._lock:
|
|
255
|
+
self._ensure_file_open()
|
|
256
|
+
for event in events:
|
|
257
|
+
line = json.dumps(event.to_dict()) + "\n"
|
|
258
|
+
self._current_file.write(line)
|
|
259
|
+
self._current_file.flush()
|
|
260
|
+
|
|
261
|
+
def read(self, event_id: str) -> AuditEvent | None:
|
|
262
|
+
"""Read a single audit event by ID."""
|
|
263
|
+
# Search through all files
|
|
264
|
+
for filepath in self._get_all_files():
|
|
265
|
+
try:
|
|
266
|
+
for event in self._read_file(filepath):
|
|
267
|
+
if event.id == event_id:
|
|
268
|
+
return event
|
|
269
|
+
except Exception:
|
|
270
|
+
continue
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
def query(
|
|
274
|
+
self,
|
|
275
|
+
start_time: datetime | None = None,
|
|
276
|
+
end_time: datetime | None = None,
|
|
277
|
+
event_types: list[AuditEventType] | None = None,
|
|
278
|
+
actor_id: str | None = None,
|
|
279
|
+
resource_id: str | None = None,
|
|
280
|
+
outcome: AuditOutcome | None = None,
|
|
281
|
+
limit: int = 100,
|
|
282
|
+
offset: int = 0,
|
|
283
|
+
) -> list[AuditEvent]:
|
|
284
|
+
"""Query audit events."""
|
|
285
|
+
results = []
|
|
286
|
+
skipped = 0
|
|
287
|
+
|
|
288
|
+
for filepath in reversed(self._get_all_files()): # Most recent first
|
|
289
|
+
if len(results) >= limit:
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
for event in self._read_file(filepath):
|
|
294
|
+
if self._matches_filters(
|
|
295
|
+
event,
|
|
296
|
+
start_time,
|
|
297
|
+
end_time,
|
|
298
|
+
event_types,
|
|
299
|
+
actor_id,
|
|
300
|
+
resource_id,
|
|
301
|
+
outcome,
|
|
302
|
+
):
|
|
303
|
+
if skipped < offset:
|
|
304
|
+
skipped += 1
|
|
305
|
+
else:
|
|
306
|
+
results.append(event)
|
|
307
|
+
if len(results) >= limit:
|
|
308
|
+
break
|
|
309
|
+
except Exception:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
return results
|
|
313
|
+
|
|
314
|
+
def count(
|
|
315
|
+
self,
|
|
316
|
+
start_time: datetime | None = None,
|
|
317
|
+
end_time: datetime | None = None,
|
|
318
|
+
event_types: list[AuditEventType] | None = None,
|
|
319
|
+
) -> int:
|
|
320
|
+
"""Count matching audit events."""
|
|
321
|
+
count = 0
|
|
322
|
+
|
|
323
|
+
for filepath in self._get_all_files():
|
|
324
|
+
try:
|
|
325
|
+
for event in self._read_file(filepath):
|
|
326
|
+
if self._matches_filters(
|
|
327
|
+
event,
|
|
328
|
+
start_time,
|
|
329
|
+
end_time,
|
|
330
|
+
event_types,
|
|
331
|
+
None,
|
|
332
|
+
None,
|
|
333
|
+
None,
|
|
334
|
+
):
|
|
335
|
+
count += 1
|
|
336
|
+
except Exception:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
return count
|
|
340
|
+
|
|
341
|
+
def delete_before(self, before: datetime) -> int:
|
|
342
|
+
"""Delete events before a given time."""
|
|
343
|
+
deleted = 0
|
|
344
|
+
|
|
345
|
+
for filepath in self._get_all_files():
|
|
346
|
+
# Check if entire file is before cutoff
|
|
347
|
+
try:
|
|
348
|
+
file_date = self._extract_date_from_filename(filepath)
|
|
349
|
+
if file_date and file_date.date() < before.date():
|
|
350
|
+
os.remove(filepath)
|
|
351
|
+
deleted += 1 # Count as 1 per file
|
|
352
|
+
except Exception:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
return deleted
|
|
356
|
+
|
|
357
|
+
def close(self) -> None:
|
|
358
|
+
"""Close storage."""
|
|
359
|
+
with self._lock:
|
|
360
|
+
if self._current_file:
|
|
361
|
+
self._current_file.close()
|
|
362
|
+
self._current_file = None
|
|
363
|
+
|
|
364
|
+
def flush(self) -> None:
|
|
365
|
+
"""Flush buffered data."""
|
|
366
|
+
with self._lock:
|
|
367
|
+
if self._current_file:
|
|
368
|
+
self._current_file.flush()
|
|
369
|
+
|
|
370
|
+
def _ensure_file_open(self) -> None:
|
|
371
|
+
"""Ensure current file is open."""
|
|
372
|
+
filename = self._get_current_filename()
|
|
373
|
+
|
|
374
|
+
if filename != self._current_filename:
|
|
375
|
+
if self._current_file:
|
|
376
|
+
self._current_file.close()
|
|
377
|
+
|
|
378
|
+
filepath = os.path.join(self._config.path, filename)
|
|
379
|
+
|
|
380
|
+
if self._config.compress:
|
|
381
|
+
self._current_file = gzip.open(
|
|
382
|
+
filepath + ".gz",
|
|
383
|
+
"at",
|
|
384
|
+
encoding=self._config.encoding,
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
self._current_file = open(
|
|
388
|
+
filepath,
|
|
389
|
+
"a",
|
|
390
|
+
encoding=self._config.encoding,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
self._current_filename = filename
|
|
394
|
+
|
|
395
|
+
def _get_current_filename(self) -> str:
|
|
396
|
+
"""Get current filename based on pattern."""
|
|
397
|
+
now = current_timestamp()
|
|
398
|
+
return self._config.filename_pattern.format(
|
|
399
|
+
date=now.strftime("%Y-%m-%d"),
|
|
400
|
+
datetime=now.strftime("%Y-%m-%d_%H"),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def _get_all_files(self) -> list[str]:
|
|
404
|
+
"""Get all audit log files sorted by date."""
|
|
405
|
+
path = Path(self._config.path)
|
|
406
|
+
files = []
|
|
407
|
+
|
|
408
|
+
for f in path.glob("audit_*.jsonl*"):
|
|
409
|
+
files.append(str(f))
|
|
410
|
+
|
|
411
|
+
return sorted(files)
|
|
412
|
+
|
|
413
|
+
def _read_file(self, filepath: str) -> Iterator[AuditEvent]:
|
|
414
|
+
"""Read events from a file."""
|
|
415
|
+
opener = gzip.open if filepath.endswith(".gz") else open
|
|
416
|
+
|
|
417
|
+
with opener(filepath, "rt", encoding=self._config.encoding) as f:
|
|
418
|
+
for line in f:
|
|
419
|
+
line = line.strip()
|
|
420
|
+
if line:
|
|
421
|
+
try:
|
|
422
|
+
data = json.loads(line)
|
|
423
|
+
yield AuditEvent.from_dict(data)
|
|
424
|
+
except (json.JSONDecodeError, TypeError):
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
def _extract_date_from_filename(self, filepath: str) -> datetime | None:
|
|
428
|
+
"""Extract date from filename."""
|
|
429
|
+
import re
|
|
430
|
+
|
|
431
|
+
match = re.search(r"audit_(\d{4}-\d{2}-\d{2})", filepath)
|
|
432
|
+
if match:
|
|
433
|
+
return datetime.strptime(match.group(1), "%Y-%m-%d").replace(
|
|
434
|
+
tzinfo=timezone.utc
|
|
435
|
+
)
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
def _matches_filters(
|
|
439
|
+
self,
|
|
440
|
+
event: AuditEvent,
|
|
441
|
+
start_time: datetime | None,
|
|
442
|
+
end_time: datetime | None,
|
|
443
|
+
event_types: list[AuditEventType] | None,
|
|
444
|
+
actor_id: str | None,
|
|
445
|
+
resource_id: str | None,
|
|
446
|
+
outcome: AuditOutcome | None,
|
|
447
|
+
) -> bool:
|
|
448
|
+
"""Check if event matches all filters."""
|
|
449
|
+
if start_time and event.timestamp < start_time:
|
|
450
|
+
return False
|
|
451
|
+
if end_time and event.timestamp > end_time:
|
|
452
|
+
return False
|
|
453
|
+
if event_types and event.event_type not in event_types:
|
|
454
|
+
return False
|
|
455
|
+
if actor_id and (not event.actor or event.actor.id != actor_id):
|
|
456
|
+
return False
|
|
457
|
+
if resource_id and (not event.resource or event.resource.id != resource_id):
|
|
458
|
+
return False
|
|
459
|
+
if outcome and event.outcome != outcome:
|
|
460
|
+
return False
|
|
461
|
+
return True
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# =============================================================================
|
|
465
|
+
# SQLite Storage
|
|
466
|
+
# =============================================================================
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class SQLiteAuditStorage(AuditStorage):
|
|
470
|
+
"""SQLite-based storage for audit events.
|
|
471
|
+
|
|
472
|
+
Provides persistent local storage with SQL query capabilities.
|
|
473
|
+
|
|
474
|
+
Example:
|
|
475
|
+
>>> storage = SQLiteAuditStorage("./audit.db")
|
|
476
|
+
>>> storage.write(event)
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
def __init__(
|
|
480
|
+
self,
|
|
481
|
+
db_path: str = "./audit.db",
|
|
482
|
+
*,
|
|
483
|
+
create_indexes: bool = True,
|
|
484
|
+
) -> None:
|
|
485
|
+
"""Initialize SQLite storage.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
db_path: Path to SQLite database file.
|
|
489
|
+
create_indexes: Create indexes for common queries.
|
|
490
|
+
"""
|
|
491
|
+
self._db_path = db_path
|
|
492
|
+
self._lock = threading.Lock()
|
|
493
|
+
self._local = threading.local()
|
|
494
|
+
|
|
495
|
+
# Initialize schema
|
|
496
|
+
self._init_schema(create_indexes)
|
|
497
|
+
|
|
498
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
499
|
+
"""Get thread-local database connection."""
|
|
500
|
+
if not hasattr(self._local, "conn"):
|
|
501
|
+
self._local.conn = sqlite3.connect(self._db_path)
|
|
502
|
+
self._local.conn.row_factory = sqlite3.Row
|
|
503
|
+
return self._local.conn
|
|
504
|
+
|
|
505
|
+
def _init_schema(self, create_indexes: bool) -> None:
|
|
506
|
+
"""Initialize database schema."""
|
|
507
|
+
conn = self._get_connection()
|
|
508
|
+
cursor = conn.cursor()
|
|
509
|
+
|
|
510
|
+
cursor.execute("""
|
|
511
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
512
|
+
id TEXT PRIMARY KEY,
|
|
513
|
+
timestamp TEXT NOT NULL,
|
|
514
|
+
timestamp_unix REAL NOT NULL,
|
|
515
|
+
event_type TEXT NOT NULL,
|
|
516
|
+
category TEXT NOT NULL,
|
|
517
|
+
severity TEXT NOT NULL,
|
|
518
|
+
action TEXT,
|
|
519
|
+
outcome TEXT NOT NULL,
|
|
520
|
+
message TEXT,
|
|
521
|
+
reason TEXT,
|
|
522
|
+
actor_id TEXT,
|
|
523
|
+
actor_type TEXT,
|
|
524
|
+
actor_name TEXT,
|
|
525
|
+
actor_ip TEXT,
|
|
526
|
+
resource_id TEXT,
|
|
527
|
+
resource_type TEXT,
|
|
528
|
+
resource_name TEXT,
|
|
529
|
+
target_id TEXT,
|
|
530
|
+
context_request_id TEXT,
|
|
531
|
+
context_trace_id TEXT,
|
|
532
|
+
context_environment TEXT,
|
|
533
|
+
data_json TEXT,
|
|
534
|
+
tags_json TEXT,
|
|
535
|
+
duration_ms REAL,
|
|
536
|
+
checksum TEXT
|
|
537
|
+
)
|
|
538
|
+
""")
|
|
539
|
+
|
|
540
|
+
if create_indexes:
|
|
541
|
+
cursor.execute(
|
|
542
|
+
"CREATE INDEX IF NOT EXISTS idx_timestamp ON audit_events(timestamp_unix)"
|
|
543
|
+
)
|
|
544
|
+
cursor.execute(
|
|
545
|
+
"CREATE INDEX IF NOT EXISTS idx_event_type ON audit_events(event_type)"
|
|
546
|
+
)
|
|
547
|
+
cursor.execute(
|
|
548
|
+
"CREATE INDEX IF NOT EXISTS idx_actor_id ON audit_events(actor_id)"
|
|
549
|
+
)
|
|
550
|
+
cursor.execute(
|
|
551
|
+
"CREATE INDEX IF NOT EXISTS idx_resource_id ON audit_events(resource_id)"
|
|
552
|
+
)
|
|
553
|
+
cursor.execute(
|
|
554
|
+
"CREATE INDEX IF NOT EXISTS idx_outcome ON audit_events(outcome)"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
conn.commit()
|
|
558
|
+
|
|
559
|
+
def write(self, event: AuditEvent) -> None:
|
|
560
|
+
"""Write a single audit event."""
|
|
561
|
+
with self._lock:
|
|
562
|
+
conn = self._get_connection()
|
|
563
|
+
self._insert_event(conn, event)
|
|
564
|
+
conn.commit()
|
|
565
|
+
|
|
566
|
+
def write_batch(self, events: list[AuditEvent]) -> None:
|
|
567
|
+
"""Write multiple audit events."""
|
|
568
|
+
with self._lock:
|
|
569
|
+
conn = self._get_connection()
|
|
570
|
+
for event in events:
|
|
571
|
+
self._insert_event(conn, event)
|
|
572
|
+
conn.commit()
|
|
573
|
+
|
|
574
|
+
def _insert_event(self, conn: sqlite3.Connection, event: AuditEvent) -> None:
|
|
575
|
+
"""Insert a single event."""
|
|
576
|
+
cursor = conn.cursor()
|
|
577
|
+
cursor.execute(
|
|
578
|
+
"""
|
|
579
|
+
INSERT OR REPLACE INTO audit_events (
|
|
580
|
+
id, timestamp, timestamp_unix, event_type, category, severity,
|
|
581
|
+
action, outcome, message, reason,
|
|
582
|
+
actor_id, actor_type, actor_name, actor_ip,
|
|
583
|
+
resource_id, resource_type, resource_name, target_id,
|
|
584
|
+
context_request_id, context_trace_id, context_environment,
|
|
585
|
+
data_json, tags_json, duration_ms, checksum
|
|
586
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
587
|
+
""",
|
|
588
|
+
(
|
|
589
|
+
event.id,
|
|
590
|
+
event.timestamp_iso,
|
|
591
|
+
event.timestamp_unix,
|
|
592
|
+
event.event_type.value,
|
|
593
|
+
event.category.value,
|
|
594
|
+
event.severity.value,
|
|
595
|
+
event.action,
|
|
596
|
+
event.outcome.value,
|
|
597
|
+
event.message,
|
|
598
|
+
event.reason,
|
|
599
|
+
event.actor.id if event.actor else None,
|
|
600
|
+
event.actor.type if event.actor else None,
|
|
601
|
+
event.actor.name if event.actor else None,
|
|
602
|
+
event.actor.ip_address if event.actor else None,
|
|
603
|
+
event.resource.id if event.resource else None,
|
|
604
|
+
event.resource.type if event.resource else None,
|
|
605
|
+
event.resource.name if event.resource else None,
|
|
606
|
+
event.target.id if event.target else None,
|
|
607
|
+
event.context.request_id,
|
|
608
|
+
event.context.trace_id,
|
|
609
|
+
event.context.environment,
|
|
610
|
+
json.dumps(event.data),
|
|
611
|
+
json.dumps(event.tags),
|
|
612
|
+
event.duration_ms,
|
|
613
|
+
event.compute_checksum(),
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
def read(self, event_id: str) -> AuditEvent | None:
|
|
618
|
+
"""Read a single audit event by ID."""
|
|
619
|
+
conn = self._get_connection()
|
|
620
|
+
cursor = conn.cursor()
|
|
621
|
+
cursor.execute("SELECT * FROM audit_events WHERE id = ?", (event_id,))
|
|
622
|
+
row = cursor.fetchone()
|
|
623
|
+
if row:
|
|
624
|
+
return self._row_to_event(row)
|
|
625
|
+
return None
|
|
626
|
+
|
|
627
|
+
def query(
|
|
628
|
+
self,
|
|
629
|
+
start_time: datetime | None = None,
|
|
630
|
+
end_time: datetime | None = None,
|
|
631
|
+
event_types: list[AuditEventType] | None = None,
|
|
632
|
+
actor_id: str | None = None,
|
|
633
|
+
resource_id: str | None = None,
|
|
634
|
+
outcome: AuditOutcome | None = None,
|
|
635
|
+
limit: int = 100,
|
|
636
|
+
offset: int = 0,
|
|
637
|
+
) -> list[AuditEvent]:
|
|
638
|
+
"""Query audit events."""
|
|
639
|
+
conn = self._get_connection()
|
|
640
|
+
cursor = conn.cursor()
|
|
641
|
+
|
|
642
|
+
query = "SELECT * FROM audit_events WHERE 1=1"
|
|
643
|
+
params: list[Any] = []
|
|
644
|
+
|
|
645
|
+
if start_time:
|
|
646
|
+
query += " AND timestamp_unix >= ?"
|
|
647
|
+
params.append(start_time.timestamp())
|
|
648
|
+
if end_time:
|
|
649
|
+
query += " AND timestamp_unix <= ?"
|
|
650
|
+
params.append(end_time.timestamp())
|
|
651
|
+
if event_types:
|
|
652
|
+
placeholders = ",".join("?" * len(event_types))
|
|
653
|
+
query += f" AND event_type IN ({placeholders})"
|
|
654
|
+
params.extend(et.value for et in event_types)
|
|
655
|
+
if actor_id:
|
|
656
|
+
query += " AND actor_id = ?"
|
|
657
|
+
params.append(actor_id)
|
|
658
|
+
if resource_id:
|
|
659
|
+
query += " AND resource_id = ?"
|
|
660
|
+
params.append(resource_id)
|
|
661
|
+
if outcome:
|
|
662
|
+
query += " AND outcome = ?"
|
|
663
|
+
params.append(outcome.value)
|
|
664
|
+
|
|
665
|
+
query += " ORDER BY timestamp_unix DESC LIMIT ? OFFSET ?"
|
|
666
|
+
params.extend([limit, offset])
|
|
667
|
+
|
|
668
|
+
cursor.execute(query, params)
|
|
669
|
+
return [self._row_to_event(row) for row in cursor.fetchall()]
|
|
670
|
+
|
|
671
|
+
def count(
|
|
672
|
+
self,
|
|
673
|
+
start_time: datetime | None = None,
|
|
674
|
+
end_time: datetime | None = None,
|
|
675
|
+
event_types: list[AuditEventType] | None = None,
|
|
676
|
+
) -> int:
|
|
677
|
+
"""Count matching audit events."""
|
|
678
|
+
conn = self._get_connection()
|
|
679
|
+
cursor = conn.cursor()
|
|
680
|
+
|
|
681
|
+
query = "SELECT COUNT(*) FROM audit_events WHERE 1=1"
|
|
682
|
+
params: list[Any] = []
|
|
683
|
+
|
|
684
|
+
if start_time:
|
|
685
|
+
query += " AND timestamp_unix >= ?"
|
|
686
|
+
params.append(start_time.timestamp())
|
|
687
|
+
if end_time:
|
|
688
|
+
query += " AND timestamp_unix <= ?"
|
|
689
|
+
params.append(end_time.timestamp())
|
|
690
|
+
if event_types:
|
|
691
|
+
placeholders = ",".join("?" * len(event_types))
|
|
692
|
+
query += f" AND event_type IN ({placeholders})"
|
|
693
|
+
params.extend(et.value for et in event_types)
|
|
694
|
+
|
|
695
|
+
cursor.execute(query, params)
|
|
696
|
+
return cursor.fetchone()[0]
|
|
697
|
+
|
|
698
|
+
def delete_before(self, before: datetime) -> int:
|
|
699
|
+
"""Delete events before a given time."""
|
|
700
|
+
with self._lock:
|
|
701
|
+
conn = self._get_connection()
|
|
702
|
+
cursor = conn.cursor()
|
|
703
|
+
cursor.execute(
|
|
704
|
+
"DELETE FROM audit_events WHERE timestamp_unix < ?",
|
|
705
|
+
(before.timestamp(),),
|
|
706
|
+
)
|
|
707
|
+
deleted = cursor.rowcount
|
|
708
|
+
conn.commit()
|
|
709
|
+
return deleted
|
|
710
|
+
|
|
711
|
+
def close(self) -> None:
|
|
712
|
+
"""Close storage."""
|
|
713
|
+
if hasattr(self._local, "conn"):
|
|
714
|
+
self._local.conn.close()
|
|
715
|
+
del self._local.conn
|
|
716
|
+
|
|
717
|
+
def _row_to_event(self, row: sqlite3.Row) -> AuditEvent:
|
|
718
|
+
"""Convert database row to AuditEvent."""
|
|
719
|
+
from truthound.audit.core import (
|
|
720
|
+
AuditActor,
|
|
721
|
+
AuditResource,
|
|
722
|
+
AuditContext,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
actor = None
|
|
726
|
+
if row["actor_id"]:
|
|
727
|
+
actor = AuditActor(
|
|
728
|
+
id=row["actor_id"],
|
|
729
|
+
type=row["actor_type"] or "user",
|
|
730
|
+
name=row["actor_name"] or "",
|
|
731
|
+
ip_address=row["actor_ip"] or "",
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
resource = None
|
|
735
|
+
if row["resource_id"]:
|
|
736
|
+
resource = AuditResource(
|
|
737
|
+
id=row["resource_id"],
|
|
738
|
+
type=row["resource_type"] or "",
|
|
739
|
+
name=row["resource_name"] or "",
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
target = None
|
|
743
|
+
if row["target_id"]:
|
|
744
|
+
target = AuditResource(
|
|
745
|
+
id=row["target_id"],
|
|
746
|
+
type="",
|
|
747
|
+
name="",
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
context = AuditContext(
|
|
751
|
+
request_id=row["context_request_id"] or "",
|
|
752
|
+
trace_id=row["context_trace_id"] or "",
|
|
753
|
+
environment=row["context_environment"] or "",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
timestamp = datetime.fromisoformat(
|
|
757
|
+
row["timestamp"].replace("Z", "+00:00")
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
return AuditEvent(
|
|
761
|
+
id=row["id"],
|
|
762
|
+
timestamp=timestamp,
|
|
763
|
+
event_type=AuditEventType(row["event_type"]),
|
|
764
|
+
category=AuditCategory(row["category"]),
|
|
765
|
+
severity=AuditSeverity(row["severity"]),
|
|
766
|
+
action=row["action"] or "",
|
|
767
|
+
outcome=AuditOutcome(row["outcome"]),
|
|
768
|
+
message=row["message"] or "",
|
|
769
|
+
reason=row["reason"] or "",
|
|
770
|
+
actor=actor,
|
|
771
|
+
resource=resource,
|
|
772
|
+
target=target,
|
|
773
|
+
context=context,
|
|
774
|
+
data=json.loads(row["data_json"]) if row["data_json"] else {},
|
|
775
|
+
tags=json.loads(row["tags_json"]) if row["tags_json"] else [],
|
|
776
|
+
duration_ms=row["duration_ms"],
|
|
777
|
+
checksum=row["checksum"] or "",
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
# Need to import these for the _row_to_event method
|
|
782
|
+
from truthound.audit.core import AuditCategory, AuditSeverity
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# =============================================================================
|
|
786
|
+
# Async File Storage
|
|
787
|
+
# =============================================================================
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class AsyncBufferedStorage(AuditStorage):
|
|
791
|
+
"""Buffered storage that writes asynchronously.
|
|
792
|
+
|
|
793
|
+
Wraps any storage backend with buffering for improved performance.
|
|
794
|
+
|
|
795
|
+
Example:
|
|
796
|
+
>>> base_storage = FileAuditStorage(config)
|
|
797
|
+
>>> storage = AsyncBufferedStorage(
|
|
798
|
+
... base_storage,
|
|
799
|
+
... buffer_size=100,
|
|
800
|
+
... flush_interval=5.0,
|
|
801
|
+
... )
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
def __init__(
|
|
805
|
+
self,
|
|
806
|
+
storage: AuditStorage,
|
|
807
|
+
buffer_size: int = 100,
|
|
808
|
+
flush_interval: float = 5.0,
|
|
809
|
+
) -> None:
|
|
810
|
+
"""Initialize async buffered storage.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
storage: Underlying storage backend.
|
|
814
|
+
buffer_size: Maximum buffer size before auto-flush.
|
|
815
|
+
flush_interval: Seconds between auto-flushes.
|
|
816
|
+
"""
|
|
817
|
+
self._storage = storage
|
|
818
|
+
self._buffer: list[AuditEvent] = []
|
|
819
|
+
self._buffer_size = buffer_size
|
|
820
|
+
self._flush_interval = flush_interval
|
|
821
|
+
self._lock = threading.Lock()
|
|
822
|
+
self._stop_event = threading.Event()
|
|
823
|
+
self._flush_thread: threading.Thread | None = None
|
|
824
|
+
|
|
825
|
+
# Start flush thread
|
|
826
|
+
self._start_flush_thread()
|
|
827
|
+
|
|
828
|
+
def _start_flush_thread(self) -> None:
|
|
829
|
+
"""Start background flush thread."""
|
|
830
|
+
def flush_loop() -> None:
|
|
831
|
+
while not self._stop_event.wait(self._flush_interval):
|
|
832
|
+
self.flush()
|
|
833
|
+
|
|
834
|
+
self._flush_thread = threading.Thread(target=flush_loop, daemon=True)
|
|
835
|
+
self._flush_thread.start()
|
|
836
|
+
|
|
837
|
+
def write(self, event: AuditEvent) -> None:
|
|
838
|
+
"""Write event to buffer."""
|
|
839
|
+
with self._lock:
|
|
840
|
+
self._buffer.append(event)
|
|
841
|
+
if len(self._buffer) >= self._buffer_size:
|
|
842
|
+
self._flush_internal()
|
|
843
|
+
|
|
844
|
+
def write_batch(self, events: list[AuditEvent]) -> None:
|
|
845
|
+
"""Write events to buffer."""
|
|
846
|
+
with self._lock:
|
|
847
|
+
self._buffer.extend(events)
|
|
848
|
+
if len(self._buffer) >= self._buffer_size:
|
|
849
|
+
self._flush_internal()
|
|
850
|
+
|
|
851
|
+
def read(self, event_id: str) -> AuditEvent | None:
|
|
852
|
+
"""Read from underlying storage."""
|
|
853
|
+
# Check buffer first
|
|
854
|
+
with self._lock:
|
|
855
|
+
for event in self._buffer:
|
|
856
|
+
if event.id == event_id:
|
|
857
|
+
return event
|
|
858
|
+
|
|
859
|
+
return self._storage.read(event_id)
|
|
860
|
+
|
|
861
|
+
def query(
|
|
862
|
+
self,
|
|
863
|
+
start_time: datetime | None = None,
|
|
864
|
+
end_time: datetime | None = None,
|
|
865
|
+
event_types: list[AuditEventType] | None = None,
|
|
866
|
+
actor_id: str | None = None,
|
|
867
|
+
resource_id: str | None = None,
|
|
868
|
+
outcome: AuditOutcome | None = None,
|
|
869
|
+
limit: int = 100,
|
|
870
|
+
offset: int = 0,
|
|
871
|
+
) -> list[AuditEvent]:
|
|
872
|
+
"""Query from underlying storage."""
|
|
873
|
+
# Flush buffer first to ensure complete results
|
|
874
|
+
self.flush()
|
|
875
|
+
return self._storage.query(
|
|
876
|
+
start_time,
|
|
877
|
+
end_time,
|
|
878
|
+
event_types,
|
|
879
|
+
actor_id,
|
|
880
|
+
resource_id,
|
|
881
|
+
outcome,
|
|
882
|
+
limit,
|
|
883
|
+
offset,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
def count(
|
|
887
|
+
self,
|
|
888
|
+
start_time: datetime | None = None,
|
|
889
|
+
end_time: datetime | None = None,
|
|
890
|
+
event_types: list[AuditEventType] | None = None,
|
|
891
|
+
) -> int:
|
|
892
|
+
"""Count from underlying storage."""
|
|
893
|
+
self.flush()
|
|
894
|
+
return self._storage.count(start_time, end_time, event_types)
|
|
895
|
+
|
|
896
|
+
def delete_before(self, before: datetime) -> int:
|
|
897
|
+
"""Delete from underlying storage."""
|
|
898
|
+
self.flush()
|
|
899
|
+
return self._storage.delete_before(before)
|
|
900
|
+
|
|
901
|
+
def flush(self) -> None:
|
|
902
|
+
"""Flush buffer to storage."""
|
|
903
|
+
with self._lock:
|
|
904
|
+
self._flush_internal()
|
|
905
|
+
|
|
906
|
+
def _flush_internal(self) -> None:
|
|
907
|
+
"""Internal flush without lock."""
|
|
908
|
+
if self._buffer:
|
|
909
|
+
try:
|
|
910
|
+
self._storage.write_batch(self._buffer)
|
|
911
|
+
self._buffer = []
|
|
912
|
+
except Exception as e:
|
|
913
|
+
# Keep events in buffer on failure
|
|
914
|
+
raise AuditStorageError(f"Failed to flush buffer: {e}") from e
|
|
915
|
+
|
|
916
|
+
def close(self) -> None:
|
|
917
|
+
"""Close storage."""
|
|
918
|
+
self._stop_event.set()
|
|
919
|
+
if self._flush_thread:
|
|
920
|
+
self._flush_thread.join(timeout=self._flush_interval * 2)
|
|
921
|
+
self.flush()
|
|
922
|
+
self._storage.close()
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
# =============================================================================
|
|
926
|
+
# Composite Storage
|
|
927
|
+
# =============================================================================
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
class CompositeAuditStorage(AuditStorage):
|
|
931
|
+
"""Storage that writes to multiple backends.
|
|
932
|
+
|
|
933
|
+
Useful for writing to both fast (memory) and persistent (file/db) storage.
|
|
934
|
+
|
|
935
|
+
Example:
|
|
936
|
+
>>> storage = CompositeAuditStorage([
|
|
937
|
+
... MemoryAuditStorage(max_events=1000),
|
|
938
|
+
... SQLiteAuditStorage("./audit.db"),
|
|
939
|
+
... ])
|
|
940
|
+
"""
|
|
941
|
+
|
|
942
|
+
def __init__(
|
|
943
|
+
self,
|
|
944
|
+
storages: list[AuditStorage],
|
|
945
|
+
*,
|
|
946
|
+
primary_index: int = 0,
|
|
947
|
+
fail_fast: bool = False,
|
|
948
|
+
) -> None:
|
|
949
|
+
"""Initialize composite storage.
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
storages: List of storage backends.
|
|
953
|
+
primary_index: Index of primary storage for reads.
|
|
954
|
+
fail_fast: Fail if any storage fails.
|
|
955
|
+
"""
|
|
956
|
+
self._storages = storages
|
|
957
|
+
self._primary_index = primary_index
|
|
958
|
+
self._fail_fast = fail_fast
|
|
959
|
+
|
|
960
|
+
@property
|
|
961
|
+
def primary(self) -> AuditStorage:
|
|
962
|
+
"""Get primary storage."""
|
|
963
|
+
return self._storages[self._primary_index]
|
|
964
|
+
|
|
965
|
+
def write(self, event: AuditEvent) -> None:
|
|
966
|
+
"""Write to all storages."""
|
|
967
|
+
errors = []
|
|
968
|
+
for storage in self._storages:
|
|
969
|
+
try:
|
|
970
|
+
storage.write(event)
|
|
971
|
+
except Exception as e:
|
|
972
|
+
if self._fail_fast:
|
|
973
|
+
raise
|
|
974
|
+
errors.append(e)
|
|
975
|
+
|
|
976
|
+
if errors and len(errors) == len(self._storages):
|
|
977
|
+
raise AuditStorageError(f"All storages failed: {errors}")
|
|
978
|
+
|
|
979
|
+
def write_batch(self, events: list[AuditEvent]) -> None:
|
|
980
|
+
"""Write batch to all storages."""
|
|
981
|
+
errors = []
|
|
982
|
+
for storage in self._storages:
|
|
983
|
+
try:
|
|
984
|
+
storage.write_batch(events)
|
|
985
|
+
except Exception as e:
|
|
986
|
+
if self._fail_fast:
|
|
987
|
+
raise
|
|
988
|
+
errors.append(e)
|
|
989
|
+
|
|
990
|
+
if errors and len(errors) == len(self._storages):
|
|
991
|
+
raise AuditStorageError(f"All storages failed: {errors}")
|
|
992
|
+
|
|
993
|
+
def read(self, event_id: str) -> AuditEvent | None:
|
|
994
|
+
"""Read from primary storage."""
|
|
995
|
+
return self.primary.read(event_id)
|
|
996
|
+
|
|
997
|
+
def query(
|
|
998
|
+
self,
|
|
999
|
+
start_time: datetime | None = None,
|
|
1000
|
+
end_time: datetime | None = None,
|
|
1001
|
+
event_types: list[AuditEventType] | None = None,
|
|
1002
|
+
actor_id: str | None = None,
|
|
1003
|
+
resource_id: str | None = None,
|
|
1004
|
+
outcome: AuditOutcome | None = None,
|
|
1005
|
+
limit: int = 100,
|
|
1006
|
+
offset: int = 0,
|
|
1007
|
+
) -> list[AuditEvent]:
|
|
1008
|
+
"""Query from primary storage."""
|
|
1009
|
+
return self.primary.query(
|
|
1010
|
+
start_time,
|
|
1011
|
+
end_time,
|
|
1012
|
+
event_types,
|
|
1013
|
+
actor_id,
|
|
1014
|
+
resource_id,
|
|
1015
|
+
outcome,
|
|
1016
|
+
limit,
|
|
1017
|
+
offset,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
def count(
|
|
1021
|
+
self,
|
|
1022
|
+
start_time: datetime | None = None,
|
|
1023
|
+
end_time: datetime | None = None,
|
|
1024
|
+
event_types: list[AuditEventType] | None = None,
|
|
1025
|
+
) -> int:
|
|
1026
|
+
"""Count from primary storage."""
|
|
1027
|
+
return self.primary.count(start_time, end_time, event_types)
|
|
1028
|
+
|
|
1029
|
+
def delete_before(self, before: datetime) -> int:
|
|
1030
|
+
"""Delete from all storages."""
|
|
1031
|
+
max_deleted = 0
|
|
1032
|
+
for storage in self._storages:
|
|
1033
|
+
try:
|
|
1034
|
+
deleted = storage.delete_before(before)
|
|
1035
|
+
max_deleted = max(max_deleted, deleted)
|
|
1036
|
+
except Exception:
|
|
1037
|
+
if self._fail_fast:
|
|
1038
|
+
raise
|
|
1039
|
+
return max_deleted
|
|
1040
|
+
|
|
1041
|
+
def flush(self) -> None:
|
|
1042
|
+
"""Flush all storages."""
|
|
1043
|
+
for storage in self._storages:
|
|
1044
|
+
storage.flush()
|
|
1045
|
+
|
|
1046
|
+
def close(self) -> None:
|
|
1047
|
+
"""Close all storages."""
|
|
1048
|
+
for storage in self._storages:
|
|
1049
|
+
storage.close()
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
# =============================================================================
|
|
1053
|
+
# Storage Factory
|
|
1054
|
+
# =============================================================================
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def create_storage(
|
|
1058
|
+
backend: str = "memory",
|
|
1059
|
+
**kwargs: Any,
|
|
1060
|
+
) -> AuditStorage:
|
|
1061
|
+
"""Create storage backend from configuration.
|
|
1062
|
+
|
|
1063
|
+
Args:
|
|
1064
|
+
backend: Storage backend type.
|
|
1065
|
+
**kwargs: Backend-specific configuration.
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
Storage instance.
|
|
1069
|
+
|
|
1070
|
+
Example:
|
|
1071
|
+
>>> storage = create_storage("memory", max_events=10000)
|
|
1072
|
+
>>> storage = create_storage("file", path="./audit_logs")
|
|
1073
|
+
>>> storage = create_storage("sqlite", db_path="./audit.db")
|
|
1074
|
+
"""
|
|
1075
|
+
if backend == "memory":
|
|
1076
|
+
return MemoryAuditStorage(**kwargs)
|
|
1077
|
+
elif backend == "file":
|
|
1078
|
+
config = FileStorageConfig(**kwargs)
|
|
1079
|
+
return FileAuditStorage(config)
|
|
1080
|
+
elif backend == "sqlite":
|
|
1081
|
+
return SQLiteAuditStorage(**kwargs)
|
|
1082
|
+
else:
|
|
1083
|
+
raise ValueError(f"Unknown storage backend: {backend}")
|