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,902 @@
|
|
|
1
|
+
"""Base classes and protocols for OIDC authentication.
|
|
2
|
+
|
|
3
|
+
This module defines the core abstractions for OIDC-based authentication,
|
|
4
|
+
providing a pluggable architecture for different identity providers and
|
|
5
|
+
cloud platforms.
|
|
6
|
+
|
|
7
|
+
Design Principles:
|
|
8
|
+
1. Protocol-based: Duck typing for flexibility
|
|
9
|
+
2. Token immutability: OIDCToken is immutable after creation
|
|
10
|
+
3. Secure by default: Tokens are redacted in repr/str
|
|
11
|
+
4. Extensible: Easy to add new providers and exchangers
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import (
|
|
25
|
+
TYPE_CHECKING,
|
|
26
|
+
Any,
|
|
27
|
+
Callable,
|
|
28
|
+
Generic,
|
|
29
|
+
Protocol,
|
|
30
|
+
TypeVar,
|
|
31
|
+
runtime_checkable,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Exceptions
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OIDCError(Exception):
|
|
47
|
+
"""Base exception for OIDC-related errors."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OIDCTokenError(OIDCError):
|
|
53
|
+
"""Raised when there's an error with the OIDC token."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, message: str, provider: str | None = None) -> None:
|
|
56
|
+
self.provider = provider
|
|
57
|
+
msg = message
|
|
58
|
+
if provider:
|
|
59
|
+
msg = f"[{provider}] {message}"
|
|
60
|
+
super().__init__(msg)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OIDCExchangeError(OIDCError):
|
|
64
|
+
"""Raised when token exchange fails."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
message: str,
|
|
69
|
+
cloud_provider: str,
|
|
70
|
+
status_code: int | None = None,
|
|
71
|
+
response: str | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self.cloud_provider = cloud_provider
|
|
74
|
+
self.status_code = status_code
|
|
75
|
+
self.response = response
|
|
76
|
+
msg = f"[{cloud_provider}] Token exchange failed: {message}"
|
|
77
|
+
if status_code:
|
|
78
|
+
msg += f" (status: {status_code})"
|
|
79
|
+
super().__init__(msg)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OIDCConfigurationError(OIDCError):
|
|
83
|
+
"""Raised when OIDC configuration is invalid."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, message: str, field: str | None = None) -> None:
|
|
86
|
+
self.field = field
|
|
87
|
+
msg = f"OIDC configuration error: {message}"
|
|
88
|
+
if field:
|
|
89
|
+
msg += f" (field: {field})"
|
|
90
|
+
super().__init__(msg)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class OIDCProviderNotAvailableError(OIDCError):
|
|
94
|
+
"""Raised when OIDC provider is not available in the current environment."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, provider: str, reason: str) -> None:
|
|
97
|
+
self.provider = provider
|
|
98
|
+
self.reason = reason
|
|
99
|
+
super().__init__(
|
|
100
|
+
f"OIDC provider '{provider}' is not available: {reason}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# =============================================================================
|
|
105
|
+
# Cloud Provider Enum
|
|
106
|
+
# =============================================================================
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CloudProvider(str, Enum):
|
|
110
|
+
"""Supported cloud providers for token exchange."""
|
|
111
|
+
|
|
112
|
+
AWS = "aws"
|
|
113
|
+
GCP = "gcp"
|
|
114
|
+
AZURE = "azure"
|
|
115
|
+
VAULT = "vault"
|
|
116
|
+
|
|
117
|
+
def __str__(self) -> str:
|
|
118
|
+
return self.value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CIProvider(str, Enum):
|
|
122
|
+
"""Supported CI/CD providers with OIDC support."""
|
|
123
|
+
|
|
124
|
+
GITHUB_ACTIONS = "github_actions"
|
|
125
|
+
GITLAB_CI = "gitlab_ci"
|
|
126
|
+
CIRCLECI = "circleci"
|
|
127
|
+
BITBUCKET = "bitbucket"
|
|
128
|
+
JENKINS = "jenkins"
|
|
129
|
+
UNKNOWN = "unknown"
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
return self.value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# OIDC Token and Claims
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class OIDCClaims:
|
|
142
|
+
"""Parsed OIDC token claims.
|
|
143
|
+
|
|
144
|
+
Common claims across different CI providers with optional
|
|
145
|
+
provider-specific extensions.
|
|
146
|
+
|
|
147
|
+
Attributes:
|
|
148
|
+
issuer: Token issuer (iss claim).
|
|
149
|
+
subject: Subject identifier (sub claim).
|
|
150
|
+
audience: Token audience (aud claim).
|
|
151
|
+
expiration: Token expiration time (exp claim).
|
|
152
|
+
issued_at: Token issue time (iat claim).
|
|
153
|
+
repository: Repository identifier (if available).
|
|
154
|
+
ref: Git ref (branch/tag) if available.
|
|
155
|
+
sha: Git commit SHA if available.
|
|
156
|
+
actor: User/actor who triggered the workflow.
|
|
157
|
+
workflow: Workflow name if available.
|
|
158
|
+
job: Job name if available.
|
|
159
|
+
run_id: Run/pipeline ID if available.
|
|
160
|
+
environment: Deployment environment if available.
|
|
161
|
+
extra: Additional provider-specific claims.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
issuer: str
|
|
165
|
+
subject: str
|
|
166
|
+
audience: str | list[str]
|
|
167
|
+
expiration: datetime
|
|
168
|
+
issued_at: datetime
|
|
169
|
+
repository: str | None = None
|
|
170
|
+
ref: str | None = None
|
|
171
|
+
sha: str | None = None
|
|
172
|
+
actor: str | None = None
|
|
173
|
+
workflow: str | None = None
|
|
174
|
+
job: str | None = None
|
|
175
|
+
run_id: str | None = None
|
|
176
|
+
environment: str | None = None
|
|
177
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def from_jwt_payload(cls, payload: dict[str, Any]) -> "OIDCClaims":
|
|
181
|
+
"""Parse claims from JWT payload.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
payload: Decoded JWT payload.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
OIDCClaims instance.
|
|
188
|
+
"""
|
|
189
|
+
# Required claims
|
|
190
|
+
issuer = payload.get("iss", "")
|
|
191
|
+
subject = payload.get("sub", "")
|
|
192
|
+
audience = payload.get("aud", "")
|
|
193
|
+
|
|
194
|
+
# Time claims
|
|
195
|
+
exp = payload.get("exp", 0)
|
|
196
|
+
iat = payload.get("iat", 0)
|
|
197
|
+
expiration = datetime.fromtimestamp(exp) if exp else datetime.now()
|
|
198
|
+
issued_at = datetime.fromtimestamp(iat) if iat else datetime.now()
|
|
199
|
+
|
|
200
|
+
# Common optional claims (varies by provider)
|
|
201
|
+
repository = payload.get("repository") or payload.get("project_path")
|
|
202
|
+
ref = payload.get("ref") or payload.get("ref_path")
|
|
203
|
+
sha = payload.get("sha") or payload.get("commit_sha")
|
|
204
|
+
actor = payload.get("actor") or payload.get("user_login")
|
|
205
|
+
workflow = payload.get("workflow") or payload.get("pipeline_source")
|
|
206
|
+
job = payload.get("job") or payload.get("job_id")
|
|
207
|
+
run_id = payload.get("run_id") or payload.get("pipeline_id")
|
|
208
|
+
environment = payload.get("environment") or payload.get("environment_scope")
|
|
209
|
+
|
|
210
|
+
# Collect extra claims
|
|
211
|
+
known_claims = {
|
|
212
|
+
"iss", "sub", "aud", "exp", "iat", "nbf", "jti",
|
|
213
|
+
"repository", "project_path", "ref", "ref_path",
|
|
214
|
+
"sha", "commit_sha", "actor", "user_login",
|
|
215
|
+
"workflow", "pipeline_source", "job", "job_id",
|
|
216
|
+
"run_id", "pipeline_id", "environment", "environment_scope",
|
|
217
|
+
}
|
|
218
|
+
extra = {k: v for k, v in payload.items() if k not in known_claims}
|
|
219
|
+
|
|
220
|
+
return cls(
|
|
221
|
+
issuer=issuer,
|
|
222
|
+
subject=subject,
|
|
223
|
+
audience=audience,
|
|
224
|
+
expiration=expiration,
|
|
225
|
+
issued_at=issued_at,
|
|
226
|
+
repository=repository,
|
|
227
|
+
ref=ref,
|
|
228
|
+
sha=sha,
|
|
229
|
+
actor=actor,
|
|
230
|
+
workflow=workflow,
|
|
231
|
+
job=job,
|
|
232
|
+
run_id=run_id,
|
|
233
|
+
environment=environment,
|
|
234
|
+
extra=extra,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def is_expired(self) -> bool:
|
|
239
|
+
"""Check if the token is expired."""
|
|
240
|
+
return datetime.now() >= self.expiration
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def time_until_expiry(self) -> timedelta:
|
|
244
|
+
"""Get time remaining until expiration."""
|
|
245
|
+
return self.expiration - datetime.now()
|
|
246
|
+
|
|
247
|
+
def get_audience_list(self) -> list[str]:
|
|
248
|
+
"""Get audience as a list."""
|
|
249
|
+
if isinstance(self.audience, list):
|
|
250
|
+
return self.audience
|
|
251
|
+
return [self.audience] if self.audience else []
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class OIDCToken:
|
|
255
|
+
"""Container for OIDC JWT token.
|
|
256
|
+
|
|
257
|
+
Provides secure handling of the raw JWT token with:
|
|
258
|
+
- Lazy claim parsing
|
|
259
|
+
- Token not exposed in repr/str
|
|
260
|
+
- Expiration checking
|
|
261
|
+
- Hash for change detection
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
>>> token = OIDCToken(jwt_string, provider="github")
|
|
265
|
+
>>> print(token) # "OIDCToken(provider=github, expires_in=...)"
|
|
266
|
+
>>> token.get_token() # Returns raw JWT
|
|
267
|
+
>>> token.claims.subject # Parsed claims
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
__slots__ = (
|
|
271
|
+
"_token",
|
|
272
|
+
"_hash",
|
|
273
|
+
"_provider",
|
|
274
|
+
"_claims",
|
|
275
|
+
"_created_at",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
token: str,
|
|
281
|
+
*,
|
|
282
|
+
provider: str = "unknown",
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Initialize OIDC token.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
token: Raw JWT token string.
|
|
288
|
+
provider: Identity provider name.
|
|
289
|
+
"""
|
|
290
|
+
self._token = token
|
|
291
|
+
self._hash = hashlib.sha256(token.encode()).hexdigest()[:16]
|
|
292
|
+
self._provider = provider
|
|
293
|
+
self._claims: OIDCClaims | None = None
|
|
294
|
+
self._created_at = datetime.now()
|
|
295
|
+
|
|
296
|
+
def get_token(self) -> str:
|
|
297
|
+
"""Get the raw JWT token.
|
|
298
|
+
|
|
299
|
+
This is the only way to access the underlying token.
|
|
300
|
+
"""
|
|
301
|
+
return self._token
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def provider(self) -> str:
|
|
305
|
+
"""Get the identity provider name."""
|
|
306
|
+
return self._provider
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def claims(self) -> OIDCClaims:
|
|
310
|
+
"""Get parsed token claims (lazy parsing)."""
|
|
311
|
+
if self._claims is None:
|
|
312
|
+
self._claims = self._parse_claims()
|
|
313
|
+
return self._claims
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def is_expired(self) -> bool:
|
|
317
|
+
"""Check if the token is expired."""
|
|
318
|
+
return self.claims.is_expired
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def hash(self) -> str:
|
|
322
|
+
"""Get hash for change detection."""
|
|
323
|
+
return self._hash
|
|
324
|
+
|
|
325
|
+
def _parse_claims(self) -> OIDCClaims:
|
|
326
|
+
"""Parse JWT claims without verification.
|
|
327
|
+
|
|
328
|
+
Note: This only decodes the payload, it does NOT verify the signature.
|
|
329
|
+
Signature verification is done by the token exchanger (e.g., AWS STS).
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
parts = self._token.split(".")
|
|
333
|
+
if len(parts) != 3:
|
|
334
|
+
raise OIDCTokenError(
|
|
335
|
+
"Invalid JWT format", provider=self._provider
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Decode payload (add padding if needed)
|
|
339
|
+
payload_b64 = parts[1]
|
|
340
|
+
padding = 4 - len(payload_b64) % 4
|
|
341
|
+
if padding != 4:
|
|
342
|
+
payload_b64 += "=" * padding
|
|
343
|
+
|
|
344
|
+
payload_json = base64.urlsafe_b64decode(payload_b64)
|
|
345
|
+
payload = json.loads(payload_json)
|
|
346
|
+
|
|
347
|
+
return OIDCClaims.from_jwt_payload(payload)
|
|
348
|
+
|
|
349
|
+
except json.JSONDecodeError as e:
|
|
350
|
+
raise OIDCTokenError(
|
|
351
|
+
f"Invalid JWT payload: {e}", provider=self._provider
|
|
352
|
+
) from e
|
|
353
|
+
except Exception as e:
|
|
354
|
+
if isinstance(e, OIDCTokenError):
|
|
355
|
+
raise
|
|
356
|
+
raise OIDCTokenError(
|
|
357
|
+
f"Failed to parse JWT: {e}", provider=self._provider
|
|
358
|
+
) from e
|
|
359
|
+
|
|
360
|
+
def __repr__(self) -> str:
|
|
361
|
+
"""Safe representation that doesn't expose the token."""
|
|
362
|
+
try:
|
|
363
|
+
expires_in = self.claims.time_until_expiry
|
|
364
|
+
expires_str = f"{expires_in.total_seconds():.0f}s"
|
|
365
|
+
except Exception:
|
|
366
|
+
expires_str = "unknown"
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
f"OIDCToken(provider={self._provider!r}, "
|
|
370
|
+
f"expires_in={expires_str})"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def __str__(self) -> str:
|
|
374
|
+
"""String representation shows masked token."""
|
|
375
|
+
return f"OIDCToken(***)"
|
|
376
|
+
|
|
377
|
+
def __bool__(self) -> bool:
|
|
378
|
+
"""Check if token is valid and not expired."""
|
|
379
|
+
return bool(self._token) and not self.is_expired
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# =============================================================================
|
|
383
|
+
# Cloud Credentials
|
|
384
|
+
# =============================================================================
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass
|
|
388
|
+
class CloudCredentials:
|
|
389
|
+
"""Base class for cloud provider credentials.
|
|
390
|
+
|
|
391
|
+
Attributes:
|
|
392
|
+
provider: Cloud provider name.
|
|
393
|
+
expires_at: When credentials expire.
|
|
394
|
+
metadata: Additional metadata.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
provider: CloudProvider = field(default=CloudProvider.AWS)
|
|
398
|
+
expires_at: datetime | None = None
|
|
399
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def is_expired(self) -> bool:
|
|
403
|
+
"""Check if credentials are expired."""
|
|
404
|
+
if self.expires_at is None:
|
|
405
|
+
return False
|
|
406
|
+
return datetime.now() >= self.expires_at
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def time_until_expiry(self) -> timedelta | None:
|
|
410
|
+
"""Get time remaining until expiration."""
|
|
411
|
+
if self.expires_at is None:
|
|
412
|
+
return None
|
|
413
|
+
return self.expires_at - datetime.now()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@dataclass
|
|
417
|
+
class AWSCredentials(CloudCredentials):
|
|
418
|
+
"""AWS STS credentials from OIDC token exchange.
|
|
419
|
+
|
|
420
|
+
Attributes:
|
|
421
|
+
access_key_id: AWS access key ID.
|
|
422
|
+
secret_access_key: AWS secret access key.
|
|
423
|
+
session_token: AWS session token.
|
|
424
|
+
assumed_role_arn: ARN of the assumed role.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
access_key_id: str = ""
|
|
428
|
+
secret_access_key: str = ""
|
|
429
|
+
session_token: str = ""
|
|
430
|
+
assumed_role_arn: str = ""
|
|
431
|
+
|
|
432
|
+
def __post_init__(self) -> None:
|
|
433
|
+
self.provider = CloudProvider.AWS
|
|
434
|
+
|
|
435
|
+
def to_boto3_session_credentials(self) -> dict[str, str]:
|
|
436
|
+
"""Convert to boto3 session credentials format."""
|
|
437
|
+
return {
|
|
438
|
+
"aws_access_key_id": self.access_key_id,
|
|
439
|
+
"aws_secret_access_key": self.secret_access_key,
|
|
440
|
+
"aws_session_token": self.session_token,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
def to_environment_variables(self) -> dict[str, str]:
|
|
444
|
+
"""Convert to environment variable format."""
|
|
445
|
+
return {
|
|
446
|
+
"AWS_ACCESS_KEY_ID": self.access_key_id,
|
|
447
|
+
"AWS_SECRET_ACCESS_KEY": self.secret_access_key,
|
|
448
|
+
"AWS_SESSION_TOKEN": self.session_token,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
def __repr__(self) -> str:
|
|
452
|
+
"""Safe representation."""
|
|
453
|
+
return (
|
|
454
|
+
f"AWSCredentials(access_key_id={self.access_key_id[:8]}..., "
|
|
455
|
+
f"role={self.assumed_role_arn})"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@dataclass
|
|
460
|
+
class GCPCredentials(CloudCredentials):
|
|
461
|
+
"""GCP Workload Identity credentials from OIDC token exchange.
|
|
462
|
+
|
|
463
|
+
Attributes:
|
|
464
|
+
access_token: OAuth2 access token.
|
|
465
|
+
token_type: Token type (usually "Bearer").
|
|
466
|
+
service_account: Service account email.
|
|
467
|
+
project_id: GCP project ID.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
access_token: str = ""
|
|
471
|
+
token_type: str = "Bearer"
|
|
472
|
+
service_account: str = ""
|
|
473
|
+
project_id: str = ""
|
|
474
|
+
|
|
475
|
+
def __post_init__(self) -> None:
|
|
476
|
+
self.provider = CloudProvider.GCP
|
|
477
|
+
|
|
478
|
+
def to_google_credentials(self) -> dict[str, Any]:
|
|
479
|
+
"""Convert to google-auth credentials format."""
|
|
480
|
+
return {
|
|
481
|
+
"token": self.access_token,
|
|
482
|
+
"expiry": self.expires_at,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
def to_environment_variables(self) -> dict[str, str]:
|
|
486
|
+
"""Convert to environment variable format."""
|
|
487
|
+
return {
|
|
488
|
+
"GOOGLE_OAUTH_ACCESS_TOKEN": self.access_token,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
def get_authorization_header(self) -> dict[str, str]:
|
|
492
|
+
"""Get HTTP authorization header."""
|
|
493
|
+
return {"Authorization": f"{self.token_type} {self.access_token}"}
|
|
494
|
+
|
|
495
|
+
def __repr__(self) -> str:
|
|
496
|
+
"""Safe representation."""
|
|
497
|
+
return (
|
|
498
|
+
f"GCPCredentials(service_account={self.service_account}, "
|
|
499
|
+
f"project={self.project_id})"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@dataclass
|
|
504
|
+
class AzureCredentials(CloudCredentials):
|
|
505
|
+
"""Azure federated credentials from OIDC token exchange.
|
|
506
|
+
|
|
507
|
+
Attributes:
|
|
508
|
+
access_token: OAuth2 access token.
|
|
509
|
+
token_type: Token type (usually "Bearer").
|
|
510
|
+
tenant_id: Azure tenant ID.
|
|
511
|
+
client_id: Azure client/app ID.
|
|
512
|
+
subscription_id: Azure subscription ID.
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
access_token: str = ""
|
|
516
|
+
token_type: str = "Bearer"
|
|
517
|
+
tenant_id: str = ""
|
|
518
|
+
client_id: str = ""
|
|
519
|
+
subscription_id: str = ""
|
|
520
|
+
|
|
521
|
+
def __post_init__(self) -> None:
|
|
522
|
+
self.provider = CloudProvider.AZURE
|
|
523
|
+
|
|
524
|
+
def to_environment_variables(self) -> dict[str, str]:
|
|
525
|
+
"""Convert to environment variable format."""
|
|
526
|
+
return {
|
|
527
|
+
"AZURE_ACCESS_TOKEN": self.access_token,
|
|
528
|
+
"AZURE_TENANT_ID": self.tenant_id,
|
|
529
|
+
"AZURE_CLIENT_ID": self.client_id,
|
|
530
|
+
"AZURE_SUBSCRIPTION_ID": self.subscription_id,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
def get_authorization_header(self) -> dict[str, str]:
|
|
534
|
+
"""Get HTTP authorization header."""
|
|
535
|
+
return {"Authorization": f"{self.token_type} {self.access_token}"}
|
|
536
|
+
|
|
537
|
+
def __repr__(self) -> str:
|
|
538
|
+
"""Safe representation."""
|
|
539
|
+
return (
|
|
540
|
+
f"AzureCredentials(tenant={self.tenant_id}, "
|
|
541
|
+
f"client={self.client_id})"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# =============================================================================
|
|
546
|
+
# Protocols
|
|
547
|
+
# =============================================================================
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@runtime_checkable
|
|
551
|
+
class OIDCProvider(Protocol):
|
|
552
|
+
"""Protocol for OIDC identity providers.
|
|
553
|
+
|
|
554
|
+
Implementations must provide get_token() to retrieve the OIDC JWT token
|
|
555
|
+
from the CI environment.
|
|
556
|
+
|
|
557
|
+
Example:
|
|
558
|
+
>>> class MyOIDCProvider:
|
|
559
|
+
... @property
|
|
560
|
+
... def name(self) -> str:
|
|
561
|
+
... return "my-ci"
|
|
562
|
+
...
|
|
563
|
+
... def get_token(self, audience: str) -> OIDCToken:
|
|
564
|
+
... jwt = fetch_from_my_ci_environment(audience)
|
|
565
|
+
... return OIDCToken(jwt, provider=self.name)
|
|
566
|
+
...
|
|
567
|
+
... def is_available(self) -> bool:
|
|
568
|
+
... return "MY_CI_TOKEN_URL" in os.environ
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def name(self) -> str:
|
|
573
|
+
"""Provider name for identification."""
|
|
574
|
+
...
|
|
575
|
+
|
|
576
|
+
def get_token(self, audience: str | None = None) -> OIDCToken:
|
|
577
|
+
"""Retrieve OIDC token for the given audience.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
audience: Token audience (required by some providers).
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
OIDCToken containing the JWT.
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
OIDCTokenError: If token retrieval fails.
|
|
587
|
+
OIDCProviderNotAvailableError: If provider is not available.
|
|
588
|
+
"""
|
|
589
|
+
...
|
|
590
|
+
|
|
591
|
+
def is_available(self) -> bool:
|
|
592
|
+
"""Check if this provider is available in the current environment."""
|
|
593
|
+
...
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@runtime_checkable
|
|
597
|
+
class TokenExchanger(Protocol):
|
|
598
|
+
"""Protocol for cloud token exchangers.
|
|
599
|
+
|
|
600
|
+
Implementations exchange an OIDC token for cloud provider credentials.
|
|
601
|
+
|
|
602
|
+
Example:
|
|
603
|
+
>>> class MyExchanger:
|
|
604
|
+
... @property
|
|
605
|
+
... def cloud_provider(self) -> CloudProvider:
|
|
606
|
+
... return CloudProvider.AWS
|
|
607
|
+
...
|
|
608
|
+
... def exchange(self, token: OIDCToken) -> CloudCredentials:
|
|
609
|
+
... creds = call_sts_api(token.get_token())
|
|
610
|
+
... return AWSCredentials(...)
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def cloud_provider(self) -> CloudProvider:
|
|
615
|
+
"""Cloud provider this exchanger targets."""
|
|
616
|
+
...
|
|
617
|
+
|
|
618
|
+
def exchange(self, token: OIDCToken) -> CloudCredentials:
|
|
619
|
+
"""Exchange OIDC token for cloud credentials.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
token: OIDC token from CI provider.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Cloud provider credentials.
|
|
626
|
+
|
|
627
|
+
Raises:
|
|
628
|
+
OIDCExchangeError: If exchange fails.
|
|
629
|
+
"""
|
|
630
|
+
...
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
# =============================================================================
|
|
634
|
+
# Base Implementations
|
|
635
|
+
# =============================================================================
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class BaseOIDCProvider(ABC):
|
|
639
|
+
"""Abstract base class for OIDC providers.
|
|
640
|
+
|
|
641
|
+
Provides common functionality for OIDC identity providers:
|
|
642
|
+
- Token caching with configurable TTL
|
|
643
|
+
- Retry logic for token requests
|
|
644
|
+
- Logging and metrics
|
|
645
|
+
|
|
646
|
+
Subclasses must implement:
|
|
647
|
+
- _fetch_token(): Retrieve token from the CI environment
|
|
648
|
+
- name property: Provider identifier
|
|
649
|
+
- is_available(): Check if provider is usable
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
def __init__(
|
|
653
|
+
self,
|
|
654
|
+
*,
|
|
655
|
+
cache_ttl_seconds: int = 300,
|
|
656
|
+
enable_cache: bool = True,
|
|
657
|
+
retry_attempts: int = 3,
|
|
658
|
+
retry_delay_seconds: float = 1.0,
|
|
659
|
+
) -> None:
|
|
660
|
+
"""Initialize the provider.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
cache_ttl_seconds: How long to cache tokens.
|
|
664
|
+
enable_cache: Whether to enable caching.
|
|
665
|
+
retry_attempts: Number of retry attempts for token fetch.
|
|
666
|
+
retry_delay_seconds: Delay between retries.
|
|
667
|
+
"""
|
|
668
|
+
self._cache: dict[str, OIDCToken] = {}
|
|
669
|
+
self._cache_ttl = timedelta(seconds=cache_ttl_seconds)
|
|
670
|
+
self._enable_cache = enable_cache
|
|
671
|
+
self._retry_attempts = retry_attempts
|
|
672
|
+
self._retry_delay = retry_delay_seconds
|
|
673
|
+
|
|
674
|
+
@property
|
|
675
|
+
@abstractmethod
|
|
676
|
+
def name(self) -> str:
|
|
677
|
+
"""Provider name for identification."""
|
|
678
|
+
pass
|
|
679
|
+
|
|
680
|
+
@abstractmethod
|
|
681
|
+
def _fetch_token(self, audience: str | None = None) -> str:
|
|
682
|
+
"""Fetch token from the CI environment.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
audience: Token audience.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
Raw JWT token string.
|
|
689
|
+
|
|
690
|
+
Raises:
|
|
691
|
+
OIDCTokenError: If token fetch fails.
|
|
692
|
+
"""
|
|
693
|
+
pass
|
|
694
|
+
|
|
695
|
+
@abstractmethod
|
|
696
|
+
def is_available(self) -> bool:
|
|
697
|
+
"""Check if this provider is available in the current environment."""
|
|
698
|
+
pass
|
|
699
|
+
|
|
700
|
+
def get_token(self, audience: str | None = None) -> OIDCToken:
|
|
701
|
+
"""Get OIDC token with caching.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
audience: Token audience.
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
OIDCToken containing the JWT.
|
|
708
|
+
"""
|
|
709
|
+
if not self.is_available():
|
|
710
|
+
raise OIDCProviderNotAvailableError(
|
|
711
|
+
self.name,
|
|
712
|
+
"Required environment variables not found",
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
cache_key = audience or "_default_"
|
|
716
|
+
|
|
717
|
+
# Check cache
|
|
718
|
+
if self._enable_cache and cache_key in self._cache:
|
|
719
|
+
cached = self._cache[cache_key]
|
|
720
|
+
# Use token if not expired and has >30s remaining
|
|
721
|
+
if not cached.is_expired:
|
|
722
|
+
remaining = cached.claims.time_until_expiry
|
|
723
|
+
if remaining.total_seconds() > 30:
|
|
724
|
+
logger.debug(
|
|
725
|
+
f"Using cached OIDC token for {self.name} "
|
|
726
|
+
f"(expires in {remaining.total_seconds():.0f}s)"
|
|
727
|
+
)
|
|
728
|
+
return cached
|
|
729
|
+
|
|
730
|
+
# Fetch new token with retry
|
|
731
|
+
import time
|
|
732
|
+
last_error: Exception | None = None
|
|
733
|
+
|
|
734
|
+
for attempt in range(self._retry_attempts):
|
|
735
|
+
try:
|
|
736
|
+
jwt = self._fetch_token(audience)
|
|
737
|
+
token = OIDCToken(jwt, provider=self.name)
|
|
738
|
+
|
|
739
|
+
# Cache the token
|
|
740
|
+
if self._enable_cache:
|
|
741
|
+
self._cache[cache_key] = token
|
|
742
|
+
|
|
743
|
+
logger.debug(
|
|
744
|
+
f"Fetched new OIDC token from {self.name} "
|
|
745
|
+
f"(expires in {token.claims.time_until_expiry.total_seconds():.0f}s)"
|
|
746
|
+
)
|
|
747
|
+
return token
|
|
748
|
+
|
|
749
|
+
except Exception as e:
|
|
750
|
+
last_error = e
|
|
751
|
+
if attempt < self._retry_attempts - 1:
|
|
752
|
+
time.sleep(self._retry_delay * (2 ** attempt))
|
|
753
|
+
logger.warning(
|
|
754
|
+
f"Retry {attempt + 1}/{self._retry_attempts} "
|
|
755
|
+
f"for OIDC token from {self.name}: {e}"
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
if last_error:
|
|
759
|
+
if isinstance(last_error, OIDCError):
|
|
760
|
+
raise last_error
|
|
761
|
+
raise OIDCTokenError(str(last_error), provider=self.name) from last_error
|
|
762
|
+
raise OIDCTokenError("Token fetch failed", provider=self.name)
|
|
763
|
+
|
|
764
|
+
def clear_cache(self, audience: str | None = None) -> None:
|
|
765
|
+
"""Clear cached tokens.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
audience: Specific audience to clear, or None to clear all.
|
|
769
|
+
"""
|
|
770
|
+
if audience is None:
|
|
771
|
+
self._cache.clear()
|
|
772
|
+
else:
|
|
773
|
+
self._cache.pop(audience, None)
|
|
774
|
+
|
|
775
|
+
def __repr__(self) -> str:
|
|
776
|
+
return f"{self.__class__.__name__}(name={self.name!r})"
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
class BaseTokenExchanger(ABC):
|
|
780
|
+
"""Abstract base class for token exchangers.
|
|
781
|
+
|
|
782
|
+
Provides common functionality for cloud token exchange:
|
|
783
|
+
- Credential caching with configurable TTL
|
|
784
|
+
- Retry logic for exchange requests
|
|
785
|
+
- Logging and metrics
|
|
786
|
+
|
|
787
|
+
Subclasses must implement:
|
|
788
|
+
- _exchange(): Perform the actual token exchange
|
|
789
|
+
- cloud_provider property: Target cloud provider
|
|
790
|
+
"""
|
|
791
|
+
|
|
792
|
+
def __init__(
|
|
793
|
+
self,
|
|
794
|
+
*,
|
|
795
|
+
cache_ttl_seconds: int = 3600,
|
|
796
|
+
enable_cache: bool = True,
|
|
797
|
+
retry_attempts: int = 3,
|
|
798
|
+
retry_delay_seconds: float = 1.0,
|
|
799
|
+
) -> None:
|
|
800
|
+
"""Initialize the exchanger.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
cache_ttl_seconds: How long to cache credentials.
|
|
804
|
+
enable_cache: Whether to enable caching.
|
|
805
|
+
retry_attempts: Number of retry attempts.
|
|
806
|
+
retry_delay_seconds: Delay between retries.
|
|
807
|
+
"""
|
|
808
|
+
self._cache: dict[str, CloudCredentials] = {}
|
|
809
|
+
self._cache_ttl = timedelta(seconds=cache_ttl_seconds)
|
|
810
|
+
self._enable_cache = enable_cache
|
|
811
|
+
self._retry_attempts = retry_attempts
|
|
812
|
+
self._retry_delay = retry_delay_seconds
|
|
813
|
+
|
|
814
|
+
@property
|
|
815
|
+
@abstractmethod
|
|
816
|
+
def cloud_provider(self) -> CloudProvider:
|
|
817
|
+
"""Cloud provider this exchanger targets."""
|
|
818
|
+
pass
|
|
819
|
+
|
|
820
|
+
@abstractmethod
|
|
821
|
+
def _exchange(self, token: OIDCToken) -> CloudCredentials:
|
|
822
|
+
"""Perform the actual token exchange.
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
token: OIDC token to exchange.
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
Cloud provider credentials.
|
|
829
|
+
|
|
830
|
+
Raises:
|
|
831
|
+
OIDCExchangeError: If exchange fails.
|
|
832
|
+
"""
|
|
833
|
+
pass
|
|
834
|
+
|
|
835
|
+
def exchange(self, token: OIDCToken) -> CloudCredentials:
|
|
836
|
+
"""Exchange OIDC token for cloud credentials with caching.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
token: OIDC token from CI provider.
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
Cloud provider credentials.
|
|
843
|
+
"""
|
|
844
|
+
cache_key = token.hash
|
|
845
|
+
|
|
846
|
+
# Check cache
|
|
847
|
+
if self._enable_cache and cache_key in self._cache:
|
|
848
|
+
cached = self._cache[cache_key]
|
|
849
|
+
# Use credentials if not expired and has >60s remaining
|
|
850
|
+
if not cached.is_expired:
|
|
851
|
+
remaining = cached.time_until_expiry
|
|
852
|
+
if remaining and remaining.total_seconds() > 60:
|
|
853
|
+
logger.debug(
|
|
854
|
+
f"Using cached {self.cloud_provider} credentials "
|
|
855
|
+
f"(expires in {remaining.total_seconds():.0f}s)"
|
|
856
|
+
)
|
|
857
|
+
return cached
|
|
858
|
+
|
|
859
|
+
# Exchange token with retry
|
|
860
|
+
import time
|
|
861
|
+
last_error: Exception | None = None
|
|
862
|
+
|
|
863
|
+
for attempt in range(self._retry_attempts):
|
|
864
|
+
try:
|
|
865
|
+
credentials = self._exchange(token)
|
|
866
|
+
|
|
867
|
+
# Cache the credentials
|
|
868
|
+
if self._enable_cache:
|
|
869
|
+
self._cache[cache_key] = credentials
|
|
870
|
+
|
|
871
|
+
logger.debug(
|
|
872
|
+
f"Exchanged OIDC token for {self.cloud_provider} credentials"
|
|
873
|
+
)
|
|
874
|
+
return credentials
|
|
875
|
+
|
|
876
|
+
except Exception as e:
|
|
877
|
+
last_error = e
|
|
878
|
+
if attempt < self._retry_attempts - 1:
|
|
879
|
+
time.sleep(self._retry_delay * (2 ** attempt))
|
|
880
|
+
logger.warning(
|
|
881
|
+
f"Retry {attempt + 1}/{self._retry_attempts} "
|
|
882
|
+
f"for {self.cloud_provider} exchange: {e}"
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
if last_error:
|
|
886
|
+
if isinstance(last_error, OIDCError):
|
|
887
|
+
raise last_error
|
|
888
|
+
raise OIDCExchangeError(
|
|
889
|
+
str(last_error),
|
|
890
|
+
cloud_provider=str(self.cloud_provider),
|
|
891
|
+
) from last_error
|
|
892
|
+
raise OIDCExchangeError(
|
|
893
|
+
"Exchange failed",
|
|
894
|
+
cloud_provider=str(self.cloud_provider),
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def clear_cache(self) -> None:
|
|
898
|
+
"""Clear cached credentials."""
|
|
899
|
+
self._cache.clear()
|
|
900
|
+
|
|
901
|
+
def __repr__(self) -> str:
|
|
902
|
+
return f"{self.__class__.__name__}(provider={self.cloud_provider!r})"
|