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
truthound/rbac/core.py
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
"""Core types, configuration, and interfaces for RBAC.
|
|
2
|
+
|
|
3
|
+
This module provides the foundational types and interfaces for Role-Based
|
|
4
|
+
Access Control (RBAC) in Truthound, supporting flexible permission models,
|
|
5
|
+
hierarchical roles, and attribute-based access control (ABAC) extensions.
|
|
6
|
+
|
|
7
|
+
Design Principles:
|
|
8
|
+
- Separation of concerns: Permissions, Roles, and Policies are independent
|
|
9
|
+
- Extensibility: Easy to add custom permission types and policy evaluators
|
|
10
|
+
- Performance: Efficient permission checking with caching
|
|
11
|
+
- Security: Deny by default, explicit grants required
|
|
12
|
+
- Multi-tenancy: Full integration with tenant context
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from contextvars import ContextVar
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from enum import Enum, Flag, auto
|
|
27
|
+
from typing import (
|
|
28
|
+
TYPE_CHECKING,
|
|
29
|
+
Any,
|
|
30
|
+
Callable,
|
|
31
|
+
FrozenSet,
|
|
32
|
+
Iterator,
|
|
33
|
+
Mapping,
|
|
34
|
+
Protocol,
|
|
35
|
+
Sequence,
|
|
36
|
+
TypeVar,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Enums and Flags
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PermissionAction(Enum):
|
|
49
|
+
"""Standard permission actions."""
|
|
50
|
+
|
|
51
|
+
# CRUD operations
|
|
52
|
+
CREATE = "create"
|
|
53
|
+
READ = "read"
|
|
54
|
+
UPDATE = "update"
|
|
55
|
+
DELETE = "delete"
|
|
56
|
+
|
|
57
|
+
# Extended operations
|
|
58
|
+
LIST = "list"
|
|
59
|
+
EXECUTE = "execute"
|
|
60
|
+
APPROVE = "approve"
|
|
61
|
+
EXPORT = "export"
|
|
62
|
+
IMPORT = "import"
|
|
63
|
+
SHARE = "share"
|
|
64
|
+
|
|
65
|
+
# Admin operations
|
|
66
|
+
MANAGE = "manage"
|
|
67
|
+
ADMIN = "admin"
|
|
68
|
+
|
|
69
|
+
# Wildcard
|
|
70
|
+
ALL = "*"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ResourceType(Enum):
|
|
74
|
+
"""Types of resources that can be protected."""
|
|
75
|
+
|
|
76
|
+
# Data resources
|
|
77
|
+
DATASET = "dataset"
|
|
78
|
+
SCHEMA = "schema"
|
|
79
|
+
VALIDATION = "validation"
|
|
80
|
+
CHECKPOINT = "checkpoint"
|
|
81
|
+
REPORT = "report"
|
|
82
|
+
|
|
83
|
+
# Configuration resources
|
|
84
|
+
CONFIG = "config"
|
|
85
|
+
SECRET = "secret"
|
|
86
|
+
WEBHOOK = "webhook"
|
|
87
|
+
|
|
88
|
+
# User management
|
|
89
|
+
USER = "user"
|
|
90
|
+
ROLE = "role"
|
|
91
|
+
PERMISSION = "permission"
|
|
92
|
+
API_KEY = "api_key"
|
|
93
|
+
|
|
94
|
+
# Tenant resources
|
|
95
|
+
TENANT = "tenant"
|
|
96
|
+
QUOTA = "quota"
|
|
97
|
+
BILLING = "billing"
|
|
98
|
+
|
|
99
|
+
# System resources
|
|
100
|
+
SYSTEM = "system"
|
|
101
|
+
AUDIT_LOG = "audit_log"
|
|
102
|
+
|
|
103
|
+
# Wildcard
|
|
104
|
+
ALL = "*"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PermissionEffect(Enum):
|
|
108
|
+
"""Effect of a permission or policy."""
|
|
109
|
+
|
|
110
|
+
ALLOW = "allow"
|
|
111
|
+
DENY = "deny"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RoleType(Enum):
|
|
115
|
+
"""Types of roles."""
|
|
116
|
+
|
|
117
|
+
SYSTEM = "system" # Built-in system roles
|
|
118
|
+
CUSTOM = "custom" # User-defined roles
|
|
119
|
+
DYNAMIC = "dynamic" # Dynamically computed roles
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PrincipalType(Enum):
|
|
123
|
+
"""Types of principals (entities that can be granted permissions)."""
|
|
124
|
+
|
|
125
|
+
USER = "user"
|
|
126
|
+
SERVICE = "service"
|
|
127
|
+
API_KEY = "api_key"
|
|
128
|
+
GROUP = "group"
|
|
129
|
+
ROLE = "role"
|
|
130
|
+
ANONYMOUS = "anonymous"
|
|
131
|
+
SYSTEM = "system"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ConditionOperator(Enum):
|
|
135
|
+
"""Operators for permission conditions."""
|
|
136
|
+
|
|
137
|
+
EQUALS = "eq"
|
|
138
|
+
NOT_EQUALS = "neq"
|
|
139
|
+
CONTAINS = "contains"
|
|
140
|
+
NOT_CONTAINS = "not_contains"
|
|
141
|
+
STARTS_WITH = "starts_with"
|
|
142
|
+
ENDS_WITH = "ends_with"
|
|
143
|
+
MATCHES = "matches" # Regex
|
|
144
|
+
IN = "in"
|
|
145
|
+
NOT_IN = "not_in"
|
|
146
|
+
GREATER_THAN = "gt"
|
|
147
|
+
LESS_THAN = "lt"
|
|
148
|
+
GREATER_THAN_OR_EQUAL = "gte"
|
|
149
|
+
LESS_THAN_OR_EQUAL = "lte"
|
|
150
|
+
EXISTS = "exists"
|
|
151
|
+
NOT_EXISTS = "not_exists"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# =============================================================================
|
|
155
|
+
# Exceptions
|
|
156
|
+
# =============================================================================
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class RBACError(Exception):
|
|
160
|
+
"""Base exception for RBAC-related errors."""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
message: str,
|
|
165
|
+
principal_id: str | None = None,
|
|
166
|
+
resource: str | None = None,
|
|
167
|
+
action: str | None = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self.principal_id = principal_id
|
|
170
|
+
self.resource = resource
|
|
171
|
+
self.action = action
|
|
172
|
+
super().__init__(message)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PermissionDeniedError(RBACError):
|
|
176
|
+
"""Raised when permission is denied."""
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class RoleNotFoundError(RBACError):
|
|
181
|
+
"""Raised when a role is not found."""
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PermissionNotFoundError(RBACError):
|
|
186
|
+
"""Raised when a permission is not found."""
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class PolicyEvaluationError(RBACError):
|
|
191
|
+
"""Raised when policy evaluation fails."""
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class InvalidPermissionError(RBACError):
|
|
196
|
+
"""Raised when a permission specification is invalid."""
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CircularRoleError(RBACError):
|
|
201
|
+
"""Raised when circular role inheritance is detected."""
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Core Data Types
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class Permission:
|
|
212
|
+
"""Represents a single permission.
|
|
213
|
+
|
|
214
|
+
Permissions follow the format: resource:action or resource:action:scope
|
|
215
|
+
Examples:
|
|
216
|
+
- dataset:read
|
|
217
|
+
- validation:execute:own
|
|
218
|
+
- checkpoint:*
|
|
219
|
+
- *:read
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> perm = Permission(
|
|
223
|
+
... resource=ResourceType.DATASET,
|
|
224
|
+
... action=PermissionAction.READ,
|
|
225
|
+
... )
|
|
226
|
+
>>> perm.to_string()
|
|
227
|
+
'dataset:read'
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
resource: ResourceType | str
|
|
231
|
+
action: PermissionAction | str
|
|
232
|
+
scope: str = "" # Optional scope: "own", "team", "tenant", "*"
|
|
233
|
+
effect: PermissionEffect = PermissionEffect.ALLOW
|
|
234
|
+
|
|
235
|
+
def __post_init__(self) -> None:
|
|
236
|
+
# Normalize resource and action to strings
|
|
237
|
+
object.__setattr__(
|
|
238
|
+
self,
|
|
239
|
+
"resource",
|
|
240
|
+
self.resource.value if isinstance(self.resource, ResourceType) else self.resource,
|
|
241
|
+
)
|
|
242
|
+
object.__setattr__(
|
|
243
|
+
self,
|
|
244
|
+
"action",
|
|
245
|
+
self.action.value if isinstance(self.action, PermissionAction) else self.action,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def to_string(self) -> str:
|
|
249
|
+
"""Convert to string format."""
|
|
250
|
+
if self.scope:
|
|
251
|
+
return f"{self.resource}:{self.action}:{self.scope}"
|
|
252
|
+
return f"{self.resource}:{self.action}"
|
|
253
|
+
|
|
254
|
+
def __str__(self) -> str:
|
|
255
|
+
return self.to_string()
|
|
256
|
+
|
|
257
|
+
def __hash__(self) -> int:
|
|
258
|
+
return hash((self.resource, self.action, self.scope, self.effect))
|
|
259
|
+
|
|
260
|
+
def matches(self, other: "Permission") -> bool:
|
|
261
|
+
"""Check if this permission matches another (supports wildcards)."""
|
|
262
|
+
# Check resource match
|
|
263
|
+
if self.resource != "*" and other.resource != "*":
|
|
264
|
+
if self.resource != other.resource:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
# Check action match
|
|
268
|
+
if self.action != "*" and other.action != "*":
|
|
269
|
+
if self.action != other.action:
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# Check scope match (empty scope matches any)
|
|
273
|
+
if self.scope and other.scope:
|
|
274
|
+
if self.scope != "*" and other.scope != "*":
|
|
275
|
+
if self.scope != other.scope:
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
return True
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def parse(cls, permission_string: str) -> "Permission":
|
|
282
|
+
"""Parse a permission string.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
permission_string: Format "resource:action" or "resource:action:scope"
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Permission object.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> Permission.parse("dataset:read")
|
|
292
|
+
Permission(resource='dataset', action='read', scope='')
|
|
293
|
+
"""
|
|
294
|
+
parts = permission_string.split(":")
|
|
295
|
+
if len(parts) < 2:
|
|
296
|
+
raise InvalidPermissionError(f"Invalid permission format: {permission_string}")
|
|
297
|
+
|
|
298
|
+
resource = parts[0]
|
|
299
|
+
action = parts[1]
|
|
300
|
+
scope = parts[2] if len(parts) > 2 else ""
|
|
301
|
+
|
|
302
|
+
return cls(resource=resource, action=action, scope=scope)
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def all(cls) -> "Permission":
|
|
306
|
+
"""Create a wildcard permission (all resources, all actions)."""
|
|
307
|
+
return cls(resource="*", action="*")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@dataclass
|
|
311
|
+
class Condition:
|
|
312
|
+
"""A condition for conditional permissions (ABAC).
|
|
313
|
+
|
|
314
|
+
Conditions allow fine-grained access control based on attributes
|
|
315
|
+
of the principal, resource, or environment.
|
|
316
|
+
|
|
317
|
+
Example:
|
|
318
|
+
>>> condition = Condition(
|
|
319
|
+
... field="resource.owner_id",
|
|
320
|
+
... operator=ConditionOperator.EQUALS,
|
|
321
|
+
... value="${principal.id}", # Dynamic reference
|
|
322
|
+
... )
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
field: str # e.g., "resource.owner_id", "principal.department"
|
|
326
|
+
operator: ConditionOperator
|
|
327
|
+
value: Any
|
|
328
|
+
description: str = ""
|
|
329
|
+
|
|
330
|
+
def evaluate(self, context: "AccessContext") -> bool:
|
|
331
|
+
"""Evaluate the condition against a context."""
|
|
332
|
+
# Get field value from context
|
|
333
|
+
actual_value = self._resolve_field(self.field, context)
|
|
334
|
+
|
|
335
|
+
# Resolve value (might be a reference like ${principal.id})
|
|
336
|
+
expected_value = self._resolve_value(self.value, context)
|
|
337
|
+
|
|
338
|
+
# Apply operator
|
|
339
|
+
return self._apply_operator(actual_value, expected_value)
|
|
340
|
+
|
|
341
|
+
def _resolve_field(self, field: str, context: "AccessContext") -> Any:
|
|
342
|
+
"""Resolve a field path to its value."""
|
|
343
|
+
parts = field.split(".")
|
|
344
|
+
if not parts:
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
# Get root object
|
|
348
|
+
root = parts[0]
|
|
349
|
+
if root == "principal":
|
|
350
|
+
obj = context.principal.__dict__ if context.principal else {}
|
|
351
|
+
elif root == "resource":
|
|
352
|
+
obj = context.resource_attributes
|
|
353
|
+
elif root == "environment":
|
|
354
|
+
obj = context.environment
|
|
355
|
+
else:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
# Navigate path
|
|
359
|
+
for part in parts[1:]:
|
|
360
|
+
if isinstance(obj, dict):
|
|
361
|
+
obj = obj.get(part)
|
|
362
|
+
elif hasattr(obj, part):
|
|
363
|
+
obj = getattr(obj, part)
|
|
364
|
+
else:
|
|
365
|
+
return None
|
|
366
|
+
if obj is None:
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
return obj
|
|
370
|
+
|
|
371
|
+
def _resolve_value(self, value: Any, context: "AccessContext") -> Any:
|
|
372
|
+
"""Resolve a value, handling dynamic references."""
|
|
373
|
+
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
|
374
|
+
field = value[2:-1]
|
|
375
|
+
return self._resolve_field(field, context)
|
|
376
|
+
return value
|
|
377
|
+
|
|
378
|
+
def _apply_operator(self, actual: Any, expected: Any) -> bool:
|
|
379
|
+
"""Apply the comparison operator."""
|
|
380
|
+
if self.operator == ConditionOperator.EQUALS:
|
|
381
|
+
return actual == expected
|
|
382
|
+
elif self.operator == ConditionOperator.NOT_EQUALS:
|
|
383
|
+
return actual != expected
|
|
384
|
+
elif self.operator == ConditionOperator.CONTAINS:
|
|
385
|
+
return expected in actual if actual else False
|
|
386
|
+
elif self.operator == ConditionOperator.NOT_CONTAINS:
|
|
387
|
+
return expected not in actual if actual else True
|
|
388
|
+
elif self.operator == ConditionOperator.STARTS_WITH:
|
|
389
|
+
return str(actual).startswith(str(expected)) if actual else False
|
|
390
|
+
elif self.operator == ConditionOperator.ENDS_WITH:
|
|
391
|
+
return str(actual).endswith(str(expected)) if actual else False
|
|
392
|
+
elif self.operator == ConditionOperator.IN:
|
|
393
|
+
return actual in expected if expected else False
|
|
394
|
+
elif self.operator == ConditionOperator.NOT_IN:
|
|
395
|
+
return actual not in expected if expected else True
|
|
396
|
+
elif self.operator == ConditionOperator.GREATER_THAN:
|
|
397
|
+
return actual > expected if actual is not None else False
|
|
398
|
+
elif self.operator == ConditionOperator.LESS_THAN:
|
|
399
|
+
return actual < expected if actual is not None else False
|
|
400
|
+
elif self.operator == ConditionOperator.GREATER_THAN_OR_EQUAL:
|
|
401
|
+
return actual >= expected if actual is not None else False
|
|
402
|
+
elif self.operator == ConditionOperator.LESS_THAN_OR_EQUAL:
|
|
403
|
+
return actual <= expected if actual is not None else False
|
|
404
|
+
elif self.operator == ConditionOperator.EXISTS:
|
|
405
|
+
return actual is not None
|
|
406
|
+
elif self.operator == ConditionOperator.NOT_EXISTS:
|
|
407
|
+
return actual is None
|
|
408
|
+
elif self.operator == ConditionOperator.MATCHES:
|
|
409
|
+
import re
|
|
410
|
+
return bool(re.match(str(expected), str(actual))) if actual else False
|
|
411
|
+
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
def to_dict(self) -> dict[str, Any]:
|
|
415
|
+
"""Convert to dictionary."""
|
|
416
|
+
return {
|
|
417
|
+
"field": self.field,
|
|
418
|
+
"operator": self.operator.value,
|
|
419
|
+
"value": self.value,
|
|
420
|
+
"description": self.description,
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@dataclass
|
|
425
|
+
class Role:
|
|
426
|
+
"""Represents a role with associated permissions.
|
|
427
|
+
|
|
428
|
+
Roles can inherit from other roles, creating a hierarchy.
|
|
429
|
+
|
|
430
|
+
Example:
|
|
431
|
+
>>> role = Role(
|
|
432
|
+
... id="data_analyst",
|
|
433
|
+
... name="Data Analyst",
|
|
434
|
+
... permissions={
|
|
435
|
+
... Permission.parse("dataset:read"),
|
|
436
|
+
... Permission.parse("validation:execute"),
|
|
437
|
+
... },
|
|
438
|
+
... )
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
id: str
|
|
442
|
+
name: str
|
|
443
|
+
description: str = ""
|
|
444
|
+
role_type: RoleType = RoleType.CUSTOM
|
|
445
|
+
|
|
446
|
+
# Permissions directly assigned to this role
|
|
447
|
+
permissions: set[Permission] = field(default_factory=set)
|
|
448
|
+
|
|
449
|
+
# Roles this role inherits from
|
|
450
|
+
parent_roles: set[str] = field(default_factory=set)
|
|
451
|
+
|
|
452
|
+
# Conditions for conditional role assignment
|
|
453
|
+
conditions: list[Condition] = field(default_factory=list)
|
|
454
|
+
|
|
455
|
+
# Metadata
|
|
456
|
+
tenant_id: str | None = None
|
|
457
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
458
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
459
|
+
created_by: str = ""
|
|
460
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
461
|
+
|
|
462
|
+
# Status
|
|
463
|
+
enabled: bool = True
|
|
464
|
+
|
|
465
|
+
def add_permission(self, permission: Permission | str) -> None:
|
|
466
|
+
"""Add a permission to this role."""
|
|
467
|
+
if isinstance(permission, str):
|
|
468
|
+
permission = Permission.parse(permission)
|
|
469
|
+
self.permissions.add(permission)
|
|
470
|
+
|
|
471
|
+
def remove_permission(self, permission: Permission | str) -> None:
|
|
472
|
+
"""Remove a permission from this role."""
|
|
473
|
+
if isinstance(permission, str):
|
|
474
|
+
permission = Permission.parse(permission)
|
|
475
|
+
self.permissions.discard(permission)
|
|
476
|
+
|
|
477
|
+
def has_permission(self, permission: Permission | str) -> bool:
|
|
478
|
+
"""Check if role has a specific permission."""
|
|
479
|
+
if isinstance(permission, str):
|
|
480
|
+
permission = Permission.parse(permission)
|
|
481
|
+
|
|
482
|
+
for p in self.permissions:
|
|
483
|
+
if p.matches(permission):
|
|
484
|
+
return True
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
def add_parent_role(self, role_id: str) -> None:
|
|
488
|
+
"""Add a parent role for inheritance."""
|
|
489
|
+
self.parent_roles.add(role_id)
|
|
490
|
+
|
|
491
|
+
def to_dict(self) -> dict[str, Any]:
|
|
492
|
+
"""Convert to dictionary."""
|
|
493
|
+
return {
|
|
494
|
+
"id": self.id,
|
|
495
|
+
"name": self.name,
|
|
496
|
+
"description": self.description,
|
|
497
|
+
"role_type": self.role_type.value,
|
|
498
|
+
"permissions": [p.to_string() for p in self.permissions],
|
|
499
|
+
"parent_roles": list(self.parent_roles),
|
|
500
|
+
"conditions": [c.to_dict() for c in self.conditions],
|
|
501
|
+
"tenant_id": self.tenant_id,
|
|
502
|
+
"created_at": self.created_at.isoformat(),
|
|
503
|
+
"updated_at": self.updated_at.isoformat(),
|
|
504
|
+
"created_by": self.created_by,
|
|
505
|
+
"metadata": self.metadata,
|
|
506
|
+
"enabled": self.enabled,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def from_dict(cls, data: dict[str, Any]) -> "Role":
|
|
511
|
+
"""Create from dictionary."""
|
|
512
|
+
return cls(
|
|
513
|
+
id=data["id"],
|
|
514
|
+
name=data["name"],
|
|
515
|
+
description=data.get("description", ""),
|
|
516
|
+
role_type=RoleType(data.get("role_type", "custom")),
|
|
517
|
+
permissions={Permission.parse(p) for p in data.get("permissions", [])},
|
|
518
|
+
parent_roles=set(data.get("parent_roles", [])),
|
|
519
|
+
tenant_id=data.get("tenant_id"),
|
|
520
|
+
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(timezone.utc),
|
|
521
|
+
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.now(timezone.utc),
|
|
522
|
+
created_by=data.get("created_by", ""),
|
|
523
|
+
metadata=data.get("metadata", {}),
|
|
524
|
+
enabled=data.get("enabled", True),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@dataclass
|
|
529
|
+
class Principal:
|
|
530
|
+
"""Represents an entity that can be granted permissions.
|
|
531
|
+
|
|
532
|
+
A principal can be a user, service, API key, or any other entity
|
|
533
|
+
that needs to access protected resources.
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
>>> principal = Principal(
|
|
537
|
+
... id="user:123",
|
|
538
|
+
... type=PrincipalType.USER,
|
|
539
|
+
... name="john.doe@example.com",
|
|
540
|
+
... roles={"data_analyst", "viewer"},
|
|
541
|
+
... )
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
id: str
|
|
545
|
+
type: PrincipalType = PrincipalType.USER
|
|
546
|
+
name: str = ""
|
|
547
|
+
email: str = ""
|
|
548
|
+
|
|
549
|
+
# Assigned roles
|
|
550
|
+
roles: set[str] = field(default_factory=set)
|
|
551
|
+
|
|
552
|
+
# Direct permissions (in addition to role permissions)
|
|
553
|
+
direct_permissions: set[Permission] = field(default_factory=set)
|
|
554
|
+
|
|
555
|
+
# Attributes for ABAC
|
|
556
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
557
|
+
|
|
558
|
+
# Tenant context
|
|
559
|
+
tenant_id: str | None = None
|
|
560
|
+
|
|
561
|
+
# Status
|
|
562
|
+
enabled: bool = True
|
|
563
|
+
|
|
564
|
+
# Metadata
|
|
565
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
566
|
+
last_active_at: datetime | None = None
|
|
567
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
568
|
+
|
|
569
|
+
@classmethod
|
|
570
|
+
def anonymous(cls) -> "Principal":
|
|
571
|
+
"""Create an anonymous principal."""
|
|
572
|
+
return cls(
|
|
573
|
+
id="anonymous",
|
|
574
|
+
type=PrincipalType.ANONYMOUS,
|
|
575
|
+
name="Anonymous",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
@classmethod
|
|
579
|
+
def system(cls) -> "Principal":
|
|
580
|
+
"""Create a system principal."""
|
|
581
|
+
return cls(
|
|
582
|
+
id="system",
|
|
583
|
+
type=PrincipalType.SYSTEM,
|
|
584
|
+
name="System",
|
|
585
|
+
roles={"system_admin"},
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def add_role(self, role_id: str) -> None:
|
|
589
|
+
"""Add a role to this principal."""
|
|
590
|
+
self.roles.add(role_id)
|
|
591
|
+
|
|
592
|
+
def remove_role(self, role_id: str) -> None:
|
|
593
|
+
"""Remove a role from this principal."""
|
|
594
|
+
self.roles.discard(role_id)
|
|
595
|
+
|
|
596
|
+
def has_role(self, role_id: str) -> bool:
|
|
597
|
+
"""Check if principal has a specific role."""
|
|
598
|
+
return role_id in self.roles
|
|
599
|
+
|
|
600
|
+
def add_permission(self, permission: Permission | str) -> None:
|
|
601
|
+
"""Add a direct permission."""
|
|
602
|
+
if isinstance(permission, str):
|
|
603
|
+
permission = Permission.parse(permission)
|
|
604
|
+
self.direct_permissions.add(permission)
|
|
605
|
+
|
|
606
|
+
def to_dict(self) -> dict[str, Any]:
|
|
607
|
+
"""Convert to dictionary."""
|
|
608
|
+
return {
|
|
609
|
+
"id": self.id,
|
|
610
|
+
"type": self.type.value,
|
|
611
|
+
"name": self.name,
|
|
612
|
+
"email": self.email,
|
|
613
|
+
"roles": list(self.roles),
|
|
614
|
+
"direct_permissions": [p.to_string() for p in self.direct_permissions],
|
|
615
|
+
"attributes": self.attributes,
|
|
616
|
+
"tenant_id": self.tenant_id,
|
|
617
|
+
"enabled": self.enabled,
|
|
618
|
+
"created_at": self.created_at.isoformat(),
|
|
619
|
+
"last_active_at": self.last_active_at.isoformat() if self.last_active_at else None,
|
|
620
|
+
"metadata": self.metadata,
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
@classmethod
|
|
624
|
+
def from_dict(cls, data: dict[str, Any]) -> "Principal":
|
|
625
|
+
"""Create from dictionary."""
|
|
626
|
+
return cls(
|
|
627
|
+
id=data["id"],
|
|
628
|
+
type=PrincipalType(data.get("type", "user")),
|
|
629
|
+
name=data.get("name", ""),
|
|
630
|
+
email=data.get("email", ""),
|
|
631
|
+
roles=set(data.get("roles", [])),
|
|
632
|
+
direct_permissions={Permission.parse(p) for p in data.get("direct_permissions", [])},
|
|
633
|
+
attributes=data.get("attributes", {}),
|
|
634
|
+
tenant_id=data.get("tenant_id"),
|
|
635
|
+
enabled=data.get("enabled", True),
|
|
636
|
+
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(timezone.utc),
|
|
637
|
+
last_active_at=datetime.fromisoformat(data["last_active_at"]) if data.get("last_active_at") else None,
|
|
638
|
+
metadata=data.get("metadata", {}),
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@dataclass
|
|
643
|
+
class AccessContext:
|
|
644
|
+
"""Context for access control decisions.
|
|
645
|
+
|
|
646
|
+
Contains all information needed to make an authorization decision,
|
|
647
|
+
including the principal, resource, action, and environment.
|
|
648
|
+
|
|
649
|
+
Example:
|
|
650
|
+
>>> context = AccessContext(
|
|
651
|
+
... principal=principal,
|
|
652
|
+
... resource="dataset:sales_data",
|
|
653
|
+
... action=PermissionAction.READ,
|
|
654
|
+
... resource_attributes={"owner_id": "user:456"},
|
|
655
|
+
... )
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
principal: Principal | None
|
|
659
|
+
resource: str
|
|
660
|
+
action: PermissionAction | str
|
|
661
|
+
resource_type: ResourceType | str | None = None
|
|
662
|
+
|
|
663
|
+
# Resource attributes for ABAC
|
|
664
|
+
resource_attributes: dict[str, Any] = field(default_factory=dict)
|
|
665
|
+
|
|
666
|
+
# Environment attributes (time, IP, etc.)
|
|
667
|
+
environment: dict[str, Any] = field(default_factory=dict)
|
|
668
|
+
|
|
669
|
+
# Request context
|
|
670
|
+
request_id: str = ""
|
|
671
|
+
tenant_id: str | None = None
|
|
672
|
+
trace_id: str = ""
|
|
673
|
+
|
|
674
|
+
def __post_init__(self) -> None:
|
|
675
|
+
# Normalize action
|
|
676
|
+
if isinstance(self.action, PermissionAction):
|
|
677
|
+
object.__setattr__(self, "action", self.action.value)
|
|
678
|
+
|
|
679
|
+
# Extract resource type from resource string if not provided
|
|
680
|
+
if self.resource_type is None and ":" in self.resource:
|
|
681
|
+
parts = self.resource.split(":")
|
|
682
|
+
object.__setattr__(self, "resource_type", parts[0])
|
|
683
|
+
|
|
684
|
+
# Add default environment attributes
|
|
685
|
+
if "timestamp" not in self.environment:
|
|
686
|
+
self.environment["timestamp"] = datetime.now(timezone.utc).isoformat()
|
|
687
|
+
|
|
688
|
+
def get_required_permission(self) -> Permission:
|
|
689
|
+
"""Get the permission required for this access."""
|
|
690
|
+
resource_type = self.resource_type or "*"
|
|
691
|
+
if isinstance(resource_type, ResourceType):
|
|
692
|
+
resource_type = resource_type.value
|
|
693
|
+
return Permission(resource=resource_type, action=self.action)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@dataclass
|
|
697
|
+
class AccessDecision:
|
|
698
|
+
"""Result of an access control decision.
|
|
699
|
+
|
|
700
|
+
Example:
|
|
701
|
+
>>> decision = AccessDecision(
|
|
702
|
+
... allowed=True,
|
|
703
|
+
... reason="Permission granted by role 'data_analyst'",
|
|
704
|
+
... matching_permissions=[Permission.parse("dataset:read")],
|
|
705
|
+
... )
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
allowed: bool
|
|
709
|
+
reason: str = ""
|
|
710
|
+
effect: PermissionEffect = PermissionEffect.DENY
|
|
711
|
+
|
|
712
|
+
# Matching permissions that led to this decision
|
|
713
|
+
matching_permissions: list[Permission] = field(default_factory=list)
|
|
714
|
+
|
|
715
|
+
# Matching policies
|
|
716
|
+
matching_policies: list[str] = field(default_factory=list)
|
|
717
|
+
|
|
718
|
+
# Evaluation time
|
|
719
|
+
evaluation_time_ms: float = 0.0
|
|
720
|
+
|
|
721
|
+
# Audit info
|
|
722
|
+
evaluated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
723
|
+
|
|
724
|
+
def __bool__(self) -> bool:
|
|
725
|
+
return self.allowed
|
|
726
|
+
|
|
727
|
+
@classmethod
|
|
728
|
+
def allow(cls, reason: str = "", permissions: list[Permission] | None = None) -> "AccessDecision":
|
|
729
|
+
"""Create an allow decision."""
|
|
730
|
+
return cls(
|
|
731
|
+
allowed=True,
|
|
732
|
+
reason=reason,
|
|
733
|
+
effect=PermissionEffect.ALLOW,
|
|
734
|
+
matching_permissions=permissions or [],
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
@classmethod
|
|
738
|
+
def deny(cls, reason: str = "") -> "AccessDecision":
|
|
739
|
+
"""Create a deny decision."""
|
|
740
|
+
return cls(
|
|
741
|
+
allowed=False,
|
|
742
|
+
reason=reason,
|
|
743
|
+
effect=PermissionEffect.DENY,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# =============================================================================
|
|
748
|
+
# Security Context (Thread-Local & ContextVar)
|
|
749
|
+
# =============================================================================
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
_current_principal: ContextVar[Principal | None] = ContextVar("current_principal", default=None)
|
|
753
|
+
_current_context: ContextVar[AccessContext | None] = ContextVar("current_access_context", default=None)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@dataclass
|
|
757
|
+
class SecurityContext:
|
|
758
|
+
"""Context for the current security principal and access context.
|
|
759
|
+
|
|
760
|
+
Provides thread-safe and async-safe context management for
|
|
761
|
+
security operations.
|
|
762
|
+
|
|
763
|
+
Example:
|
|
764
|
+
>>> with SecurityContext.set_principal(principal):
|
|
765
|
+
... current = SecurityContext.get_current_principal()
|
|
766
|
+
... assert current.id == principal.id
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
principal: Principal
|
|
770
|
+
roles: set[str] = field(default_factory=set)
|
|
771
|
+
permissions: set[Permission] = field(default_factory=set)
|
|
772
|
+
tenant_id: str | None = None
|
|
773
|
+
session_id: str = ""
|
|
774
|
+
authenticated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
775
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
776
|
+
|
|
777
|
+
@classmethod
|
|
778
|
+
def get_current_principal(cls) -> Principal | None:
|
|
779
|
+
"""Get the current principal from context."""
|
|
780
|
+
return _current_principal.get()
|
|
781
|
+
|
|
782
|
+
@classmethod
|
|
783
|
+
def require_principal(cls) -> Principal:
|
|
784
|
+
"""Get the current principal, raising if not set."""
|
|
785
|
+
principal = cls.get_current_principal()
|
|
786
|
+
if principal is None:
|
|
787
|
+
raise PermissionDeniedError("No authenticated principal")
|
|
788
|
+
return principal
|
|
789
|
+
|
|
790
|
+
@classmethod
|
|
791
|
+
@contextmanager
|
|
792
|
+
def set_principal(cls, principal: Principal) -> Iterator["SecurityContext"]:
|
|
793
|
+
"""Set the current principal for the context.
|
|
794
|
+
|
|
795
|
+
Example:
|
|
796
|
+
>>> with SecurityContext.set_principal(principal) as ctx:
|
|
797
|
+
... process_request()
|
|
798
|
+
"""
|
|
799
|
+
token = _current_principal.set(principal)
|
|
800
|
+
context = cls(principal=principal, roles=principal.roles)
|
|
801
|
+
try:
|
|
802
|
+
yield context
|
|
803
|
+
finally:
|
|
804
|
+
_current_principal.reset(token)
|
|
805
|
+
|
|
806
|
+
@classmethod
|
|
807
|
+
def clear(cls) -> None:
|
|
808
|
+
"""Clear the current security context."""
|
|
809
|
+
_current_principal.set(None)
|
|
810
|
+
_current_context.set(None)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
# =============================================================================
|
|
814
|
+
# Interfaces (Abstract Base Classes)
|
|
815
|
+
# =============================================================================
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class RoleStore(ABC):
|
|
819
|
+
"""Abstract interface for role storage."""
|
|
820
|
+
|
|
821
|
+
@abstractmethod
|
|
822
|
+
def get(self, role_id: str) -> Role | None:
|
|
823
|
+
"""Get a role by ID."""
|
|
824
|
+
...
|
|
825
|
+
|
|
826
|
+
@abstractmethod
|
|
827
|
+
def list(
|
|
828
|
+
self,
|
|
829
|
+
tenant_id: str | None = None,
|
|
830
|
+
role_type: RoleType | None = None,
|
|
831
|
+
enabled: bool | None = None,
|
|
832
|
+
) -> list[Role]:
|
|
833
|
+
"""List roles with optional filters."""
|
|
834
|
+
...
|
|
835
|
+
|
|
836
|
+
@abstractmethod
|
|
837
|
+
def save(self, role: Role) -> None:
|
|
838
|
+
"""Save a role (create or update)."""
|
|
839
|
+
...
|
|
840
|
+
|
|
841
|
+
@abstractmethod
|
|
842
|
+
def delete(self, role_id: str) -> bool:
|
|
843
|
+
"""Delete a role. Returns True if deleted."""
|
|
844
|
+
...
|
|
845
|
+
|
|
846
|
+
@abstractmethod
|
|
847
|
+
def exists(self, role_id: str) -> bool:
|
|
848
|
+
"""Check if a role exists."""
|
|
849
|
+
...
|
|
850
|
+
|
|
851
|
+
def get_all_permissions(self, role_id: str, visited: set[str] | None = None) -> set[Permission]:
|
|
852
|
+
"""Get all permissions for a role, including inherited ones."""
|
|
853
|
+
if visited is None:
|
|
854
|
+
visited = set()
|
|
855
|
+
|
|
856
|
+
if role_id in visited:
|
|
857
|
+
raise CircularRoleError(f"Circular role inheritance detected: {role_id}")
|
|
858
|
+
|
|
859
|
+
visited.add(role_id)
|
|
860
|
+
role = self.get(role_id)
|
|
861
|
+
if not role:
|
|
862
|
+
return set()
|
|
863
|
+
|
|
864
|
+
permissions = set(role.permissions)
|
|
865
|
+
|
|
866
|
+
# Add inherited permissions
|
|
867
|
+
for parent_id in role.parent_roles:
|
|
868
|
+
permissions.update(self.get_all_permissions(parent_id, visited))
|
|
869
|
+
|
|
870
|
+
return permissions
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
class PrincipalStore(ABC):
|
|
874
|
+
"""Abstract interface for principal storage."""
|
|
875
|
+
|
|
876
|
+
@abstractmethod
|
|
877
|
+
def get(self, principal_id: str) -> Principal | None:
|
|
878
|
+
"""Get a principal by ID."""
|
|
879
|
+
...
|
|
880
|
+
|
|
881
|
+
@abstractmethod
|
|
882
|
+
def get_by_email(self, email: str) -> Principal | None:
|
|
883
|
+
"""Get a principal by email."""
|
|
884
|
+
...
|
|
885
|
+
|
|
886
|
+
@abstractmethod
|
|
887
|
+
def list(
|
|
888
|
+
self,
|
|
889
|
+
tenant_id: str | None = None,
|
|
890
|
+
principal_type: PrincipalType | None = None,
|
|
891
|
+
role_id: str | None = None,
|
|
892
|
+
) -> list[Principal]:
|
|
893
|
+
"""List principals with optional filters."""
|
|
894
|
+
...
|
|
895
|
+
|
|
896
|
+
@abstractmethod
|
|
897
|
+
def save(self, principal: Principal) -> None:
|
|
898
|
+
"""Save a principal (create or update)."""
|
|
899
|
+
...
|
|
900
|
+
|
|
901
|
+
@abstractmethod
|
|
902
|
+
def delete(self, principal_id: str) -> bool:
|
|
903
|
+
"""Delete a principal. Returns True if deleted."""
|
|
904
|
+
...
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
class PolicyEvaluator(ABC):
|
|
908
|
+
"""Abstract interface for policy evaluation."""
|
|
909
|
+
|
|
910
|
+
@abstractmethod
|
|
911
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
912
|
+
"""Evaluate access for the given context."""
|
|
913
|
+
...
|
|
914
|
+
|
|
915
|
+
@property
|
|
916
|
+
@abstractmethod
|
|
917
|
+
def name(self) -> str:
|
|
918
|
+
"""Name of this evaluator."""
|
|
919
|
+
...
|
|
920
|
+
|
|
921
|
+
@property
|
|
922
|
+
def priority(self) -> int:
|
|
923
|
+
"""Priority of this evaluator (higher = evaluated first)."""
|
|
924
|
+
return 0
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
class PermissionChecker(Protocol):
|
|
928
|
+
"""Protocol for permission checking."""
|
|
929
|
+
|
|
930
|
+
def check(
|
|
931
|
+
self,
|
|
932
|
+
principal: Principal,
|
|
933
|
+
resource: str,
|
|
934
|
+
action: str | PermissionAction,
|
|
935
|
+
) -> AccessDecision:
|
|
936
|
+
"""Check if principal has permission."""
|
|
937
|
+
...
|
|
938
|
+
|
|
939
|
+
def require(
|
|
940
|
+
self,
|
|
941
|
+
principal: Principal,
|
|
942
|
+
resource: str,
|
|
943
|
+
action: str | PermissionAction,
|
|
944
|
+
) -> None:
|
|
945
|
+
"""Require permission, raising if denied."""
|
|
946
|
+
...
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
# =============================================================================
|
|
950
|
+
# Utility Functions
|
|
951
|
+
# =============================================================================
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def generate_role_id(name: str) -> str:
|
|
955
|
+
"""Generate a role ID from name."""
|
|
956
|
+
import re
|
|
957
|
+
slug = name.lower()
|
|
958
|
+
slug = re.sub(r"[^a-z0-9]+", "_", slug)
|
|
959
|
+
slug = slug.strip("_")
|
|
960
|
+
return slug
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def generate_principal_id(prefix: str = "principal") -> str:
|
|
964
|
+
"""Generate a unique principal ID."""
|
|
965
|
+
unique_part = uuid.uuid4().hex[:12]
|
|
966
|
+
return f"{prefix}_{unique_part}"
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def current_principal() -> Principal | None:
|
|
970
|
+
"""Get the current principal from context."""
|
|
971
|
+
return SecurityContext.get_current_principal()
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def require_principal() -> Principal:
|
|
975
|
+
"""Get the current principal, raising if not set."""
|
|
976
|
+
return SecurityContext.require_principal()
|