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,1020 @@
|
|
|
1
|
+
"""Dynamic Catalog Loading and Context-Based Messages.
|
|
2
|
+
|
|
3
|
+
This module provides lazy loading of message catalogs and context-aware
|
|
4
|
+
message resolution for enterprise i18n needs.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Lazy loading (only load catalogs when needed)
|
|
8
|
+
- Multiple storage backends (filesystem, package resources, URL)
|
|
9
|
+
- Caching with TTL and LRU eviction
|
|
10
|
+
- Namespace support for modular catalogs
|
|
11
|
+
- Context-based message selection
|
|
12
|
+
- Message inheritance and composition
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from truthound.validators.i18n.loader import (
|
|
16
|
+
CatalogManager,
|
|
17
|
+
MessageContext,
|
|
18
|
+
ContextResolver,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Create manager with lazy loading
|
|
22
|
+
manager = CatalogManager(
|
|
23
|
+
base_path=Path("locales"),
|
|
24
|
+
lazy=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Load catalog on demand
|
|
28
|
+
catalog = manager.get_catalog(LocaleInfo.parse("ko"))
|
|
29
|
+
|
|
30
|
+
# Context-based resolution
|
|
31
|
+
resolver = ContextResolver(manager)
|
|
32
|
+
message = resolver.resolve(
|
|
33
|
+
key="validation.failed",
|
|
34
|
+
locale=LocaleInfo.parse("ko"),
|
|
35
|
+
context=MessageContext.FORMAL,
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import logging
|
|
43
|
+
import threading
|
|
44
|
+
import time
|
|
45
|
+
import weakref
|
|
46
|
+
from collections import OrderedDict
|
|
47
|
+
from dataclasses import dataclass, field
|
|
48
|
+
from enum import Enum
|
|
49
|
+
from functools import lru_cache
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Any, Callable, Iterator
|
|
52
|
+
|
|
53
|
+
from truthound.validators.i18n.protocols import (
|
|
54
|
+
BaseCatalogLoader,
|
|
55
|
+
LocaleInfo,
|
|
56
|
+
MessageContext,
|
|
57
|
+
MessageResolver,
|
|
58
|
+
PluralizedMessage,
|
|
59
|
+
ResolvedMessage,
|
|
60
|
+
)
|
|
61
|
+
from truthound.validators.i18n.plural import get_plural_category, PluralCategory
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ==============================================================================
|
|
68
|
+
# Cache Implementation
|
|
69
|
+
# ==============================================================================
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CacheEntry:
|
|
73
|
+
"""Cache entry with metadata.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
value: Cached value
|
|
77
|
+
created_at: Creation timestamp
|
|
78
|
+
accessed_at: Last access timestamp
|
|
79
|
+
ttl: Time-to-live in seconds
|
|
80
|
+
"""
|
|
81
|
+
value: Any
|
|
82
|
+
created_at: float = field(default_factory=time.time)
|
|
83
|
+
accessed_at: float = field(default_factory=time.time)
|
|
84
|
+
ttl: float = 3600.0 # 1 hour default
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_expired(self) -> bool:
|
|
88
|
+
"""Check if entry is expired."""
|
|
89
|
+
return time.time() - self.created_at > self.ttl
|
|
90
|
+
|
|
91
|
+
def touch(self) -> None:
|
|
92
|
+
"""Update last access time."""
|
|
93
|
+
self.accessed_at = time.time()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class LRUCache:
|
|
97
|
+
"""Thread-safe LRU cache with TTL support.
|
|
98
|
+
|
|
99
|
+
Implements a Least Recently Used cache with optional
|
|
100
|
+
time-based expiration for entries.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
max_size: int = 100,
|
|
106
|
+
default_ttl: float = 3600.0,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Initialize cache.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
max_size: Maximum number of entries
|
|
112
|
+
default_ttl: Default TTL in seconds
|
|
113
|
+
"""
|
|
114
|
+
self.max_size = max_size
|
|
115
|
+
self.default_ttl = default_ttl
|
|
116
|
+
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
117
|
+
self._lock = threading.RLock()
|
|
118
|
+
|
|
119
|
+
def get(self, key: str) -> Any | None:
|
|
120
|
+
"""Get value from cache.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
key: Cache key
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Cached value or None if not found/expired
|
|
127
|
+
"""
|
|
128
|
+
with self._lock:
|
|
129
|
+
entry = self._cache.get(key)
|
|
130
|
+
if entry is None:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
if entry.is_expired:
|
|
134
|
+
del self._cache[key]
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Move to end (most recently used)
|
|
138
|
+
entry.touch()
|
|
139
|
+
self._cache.move_to_end(key)
|
|
140
|
+
return entry.value
|
|
141
|
+
|
|
142
|
+
def set(
|
|
143
|
+
self,
|
|
144
|
+
key: str,
|
|
145
|
+
value: Any,
|
|
146
|
+
ttl: float | None = None,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Set value in cache.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
key: Cache key
|
|
152
|
+
value: Value to cache
|
|
153
|
+
ttl: TTL in seconds (default from constructor)
|
|
154
|
+
"""
|
|
155
|
+
with self._lock:
|
|
156
|
+
# Remove oldest if at capacity
|
|
157
|
+
while len(self._cache) >= self.max_size:
|
|
158
|
+
self._cache.popitem(last=False)
|
|
159
|
+
|
|
160
|
+
self._cache[key] = CacheEntry(
|
|
161
|
+
value=value,
|
|
162
|
+
ttl=ttl or self.default_ttl,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def delete(self, key: str) -> None:
|
|
166
|
+
"""Delete entry from cache.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
key: Cache key
|
|
170
|
+
"""
|
|
171
|
+
with self._lock:
|
|
172
|
+
self._cache.pop(key, None)
|
|
173
|
+
|
|
174
|
+
def clear(self) -> None:
|
|
175
|
+
"""Clear all entries."""
|
|
176
|
+
with self._lock:
|
|
177
|
+
self._cache.clear()
|
|
178
|
+
|
|
179
|
+
def cleanup_expired(self) -> int:
|
|
180
|
+
"""Remove all expired entries.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Number of entries removed
|
|
184
|
+
"""
|
|
185
|
+
with self._lock:
|
|
186
|
+
expired = [k for k, v in self._cache.items() if v.is_expired]
|
|
187
|
+
for key in expired:
|
|
188
|
+
del self._cache[key]
|
|
189
|
+
return len(expired)
|
|
190
|
+
|
|
191
|
+
def __len__(self) -> int:
|
|
192
|
+
return len(self._cache)
|
|
193
|
+
|
|
194
|
+
def __contains__(self, key: str) -> bool:
|
|
195
|
+
with self._lock:
|
|
196
|
+
entry = self._cache.get(key)
|
|
197
|
+
return entry is not None and not entry.is_expired
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ==============================================================================
|
|
201
|
+
# Catalog Storage Backends
|
|
202
|
+
# ==============================================================================
|
|
203
|
+
|
|
204
|
+
class CatalogStorageBackend:
|
|
205
|
+
"""Base class for catalog storage backends."""
|
|
206
|
+
|
|
207
|
+
def load(
|
|
208
|
+
self,
|
|
209
|
+
locale: LocaleInfo,
|
|
210
|
+
namespace: str | None = None,
|
|
211
|
+
) -> dict[str, str]:
|
|
212
|
+
"""Load catalog from storage.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
locale: Target locale
|
|
216
|
+
namespace: Optional namespace
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Message dictionary
|
|
220
|
+
"""
|
|
221
|
+
raise NotImplementedError
|
|
222
|
+
|
|
223
|
+
def save(
|
|
224
|
+
self,
|
|
225
|
+
locale: LocaleInfo,
|
|
226
|
+
catalog: dict[str, str],
|
|
227
|
+
namespace: str | None = None,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Save catalog to storage.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
locale: Target locale
|
|
233
|
+
catalog: Message dictionary
|
|
234
|
+
namespace: Optional namespace
|
|
235
|
+
"""
|
|
236
|
+
raise NotImplementedError
|
|
237
|
+
|
|
238
|
+
def exists(
|
|
239
|
+
self,
|
|
240
|
+
locale: LocaleInfo,
|
|
241
|
+
namespace: str | None = None,
|
|
242
|
+
) -> bool:
|
|
243
|
+
"""Check if catalog exists.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
locale: Target locale
|
|
247
|
+
namespace: Optional namespace
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if catalog exists
|
|
251
|
+
"""
|
|
252
|
+
raise NotImplementedError
|
|
253
|
+
|
|
254
|
+
def list_locales(self, namespace: str | None = None) -> list[LocaleInfo]:
|
|
255
|
+
"""List available locales.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
namespace: Optional namespace filter
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of available locales
|
|
262
|
+
"""
|
|
263
|
+
raise NotImplementedError
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class FileSystemStorage(CatalogStorageBackend):
|
|
267
|
+
"""Filesystem-based catalog storage.
|
|
268
|
+
|
|
269
|
+
Stores catalogs as JSON files in a directory structure:
|
|
270
|
+
base_path/
|
|
271
|
+
en/
|
|
272
|
+
validators.json
|
|
273
|
+
errors.json
|
|
274
|
+
ko/
|
|
275
|
+
validators.json
|
|
276
|
+
errors.json
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
base_path: Path | str,
|
|
282
|
+
file_extension: str = ".json",
|
|
283
|
+
encoding: str = "utf-8",
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Initialize filesystem storage.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
base_path: Base directory for catalogs
|
|
289
|
+
file_extension: File extension
|
|
290
|
+
encoding: File encoding
|
|
291
|
+
"""
|
|
292
|
+
self.base_path = Path(base_path)
|
|
293
|
+
self.file_extension = file_extension
|
|
294
|
+
self.encoding = encoding
|
|
295
|
+
|
|
296
|
+
def _get_path(self, locale: LocaleInfo, namespace: str | None) -> Path:
|
|
297
|
+
"""Get path for a catalog file."""
|
|
298
|
+
locale_dir = self.base_path / locale.tag.replace("-", "_")
|
|
299
|
+
filename = f"{namespace or 'messages'}{self.file_extension}"
|
|
300
|
+
return locale_dir / filename
|
|
301
|
+
|
|
302
|
+
def load(
|
|
303
|
+
self,
|
|
304
|
+
locale: LocaleInfo,
|
|
305
|
+
namespace: str | None = None,
|
|
306
|
+
) -> dict[str, str]:
|
|
307
|
+
path = self._get_path(locale, namespace)
|
|
308
|
+
|
|
309
|
+
if not path.exists():
|
|
310
|
+
# Try fallback paths
|
|
311
|
+
fallback_paths = [
|
|
312
|
+
self.base_path / locale.language / f"{namespace or 'messages'}{self.file_extension}",
|
|
313
|
+
self.base_path / f"{locale.language}{self.file_extension}",
|
|
314
|
+
]
|
|
315
|
+
for fallback in fallback_paths:
|
|
316
|
+
if fallback.exists():
|
|
317
|
+
path = fallback
|
|
318
|
+
break
|
|
319
|
+
else:
|
|
320
|
+
return {}
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
with open(path, "r", encoding=self.encoding) as f:
|
|
324
|
+
data = json.load(f)
|
|
325
|
+
|
|
326
|
+
# Handle nested JSON structure
|
|
327
|
+
if "messages" in data:
|
|
328
|
+
return data["messages"]
|
|
329
|
+
return data
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.error(f"Failed to load catalog from {path}: {e}")
|
|
333
|
+
return {}
|
|
334
|
+
|
|
335
|
+
def save(
|
|
336
|
+
self,
|
|
337
|
+
locale: LocaleInfo,
|
|
338
|
+
catalog: dict[str, str],
|
|
339
|
+
namespace: str | None = None,
|
|
340
|
+
) -> None:
|
|
341
|
+
path = self._get_path(locale, namespace)
|
|
342
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
with open(path, "w", encoding=self.encoding) as f:
|
|
346
|
+
json.dump(
|
|
347
|
+
{"locale": locale.tag, "messages": catalog},
|
|
348
|
+
f,
|
|
349
|
+
ensure_ascii=False,
|
|
350
|
+
indent=2,
|
|
351
|
+
)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Failed to save catalog to {path}: {e}")
|
|
354
|
+
|
|
355
|
+
def exists(
|
|
356
|
+
self,
|
|
357
|
+
locale: LocaleInfo,
|
|
358
|
+
namespace: str | None = None,
|
|
359
|
+
) -> bool:
|
|
360
|
+
path = self._get_path(locale, namespace)
|
|
361
|
+
return path.exists()
|
|
362
|
+
|
|
363
|
+
def list_locales(self, namespace: str | None = None) -> list[LocaleInfo]:
|
|
364
|
+
if not self.base_path.exists():
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
locales = []
|
|
368
|
+
for item in self.base_path.iterdir():
|
|
369
|
+
if item.is_dir():
|
|
370
|
+
locale_tag = item.name.replace("_", "-")
|
|
371
|
+
try:
|
|
372
|
+
locale = LocaleInfo.parse(locale_tag)
|
|
373
|
+
if namespace is None or self.exists(locale, namespace):
|
|
374
|
+
locales.append(locale)
|
|
375
|
+
except Exception:
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
return locales
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class MemoryStorage(CatalogStorageBackend):
|
|
382
|
+
"""In-memory catalog storage for testing."""
|
|
383
|
+
|
|
384
|
+
def __init__(self) -> None:
|
|
385
|
+
self._catalogs: dict[str, dict[str, str]] = {}
|
|
386
|
+
|
|
387
|
+
def _key(self, locale: LocaleInfo, namespace: str | None) -> str:
|
|
388
|
+
return f"{locale.tag}:{namespace or 'default'}"
|
|
389
|
+
|
|
390
|
+
def load(
|
|
391
|
+
self,
|
|
392
|
+
locale: LocaleInfo,
|
|
393
|
+
namespace: str | None = None,
|
|
394
|
+
) -> dict[str, str]:
|
|
395
|
+
return self._catalogs.get(self._key(locale, namespace), {})
|
|
396
|
+
|
|
397
|
+
def save(
|
|
398
|
+
self,
|
|
399
|
+
locale: LocaleInfo,
|
|
400
|
+
catalog: dict[str, str],
|
|
401
|
+
namespace: str | None = None,
|
|
402
|
+
) -> None:
|
|
403
|
+
self._catalogs[self._key(locale, namespace)] = catalog.copy()
|
|
404
|
+
|
|
405
|
+
def exists(
|
|
406
|
+
self,
|
|
407
|
+
locale: LocaleInfo,
|
|
408
|
+
namespace: str | None = None,
|
|
409
|
+
) -> bool:
|
|
410
|
+
return self._key(locale, namespace) in self._catalogs
|
|
411
|
+
|
|
412
|
+
def list_locales(self, namespace: str | None = None) -> list[LocaleInfo]:
|
|
413
|
+
locales = []
|
|
414
|
+
ns = namespace or "default"
|
|
415
|
+
for key in self._catalogs:
|
|
416
|
+
parts = key.split(":")
|
|
417
|
+
if len(parts) == 2 and parts[1] == ns:
|
|
418
|
+
try:
|
|
419
|
+
locales.append(LocaleInfo.parse(parts[0]))
|
|
420
|
+
except Exception:
|
|
421
|
+
continue
|
|
422
|
+
return locales
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ==============================================================================
|
|
426
|
+
# Catalog Manager
|
|
427
|
+
# ==============================================================================
|
|
428
|
+
|
|
429
|
+
class CatalogManager(BaseCatalogLoader):
|
|
430
|
+
"""Manager for message catalog loading with lazy loading support.
|
|
431
|
+
|
|
432
|
+
Features:
|
|
433
|
+
- Lazy loading of catalogs on first access
|
|
434
|
+
- Caching with configurable TTL
|
|
435
|
+
- Multiple storage backend support
|
|
436
|
+
- Namespace support for modular catalogs
|
|
437
|
+
- Automatic fallback chain resolution
|
|
438
|
+
|
|
439
|
+
Example:
|
|
440
|
+
manager = CatalogManager(
|
|
441
|
+
base_path=Path("locales"),
|
|
442
|
+
lazy=True,
|
|
443
|
+
cache_ttl=3600,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# First access loads the catalog
|
|
447
|
+
catalog = manager.get_catalog(LocaleInfo.parse("ko"))
|
|
448
|
+
|
|
449
|
+
# Subsequent accesses use cache
|
|
450
|
+
catalog = manager.get_catalog(LocaleInfo.parse("ko"))
|
|
451
|
+
|
|
452
|
+
# Preload all catalogs
|
|
453
|
+
manager.preload(["en", "ko", "ja"])
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
def __init__(
|
|
457
|
+
self,
|
|
458
|
+
base_path: Path | str | None = None,
|
|
459
|
+
storage: CatalogStorageBackend | None = None,
|
|
460
|
+
lazy: bool = True,
|
|
461
|
+
cache_ttl: float = 3600.0,
|
|
462
|
+
cache_size: int = 50,
|
|
463
|
+
fallback_locale: str = "en",
|
|
464
|
+
) -> None:
|
|
465
|
+
"""Initialize catalog manager.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
base_path: Path for filesystem storage
|
|
469
|
+
storage: Custom storage backend
|
|
470
|
+
lazy: Enable lazy loading
|
|
471
|
+
cache_ttl: Cache TTL in seconds
|
|
472
|
+
cache_size: Maximum cached catalogs
|
|
473
|
+
fallback_locale: Fallback locale code
|
|
474
|
+
"""
|
|
475
|
+
super().__init__()
|
|
476
|
+
|
|
477
|
+
if storage:
|
|
478
|
+
self.storage = storage
|
|
479
|
+
elif base_path:
|
|
480
|
+
self.storage = FileSystemStorage(base_path)
|
|
481
|
+
else:
|
|
482
|
+
self.storage = MemoryStorage()
|
|
483
|
+
|
|
484
|
+
self.lazy = lazy
|
|
485
|
+
self.fallback_locale = LocaleInfo.parse(fallback_locale)
|
|
486
|
+
self._cache = LRUCache(max_size=cache_size, default_ttl=cache_ttl)
|
|
487
|
+
self._loading_locks: dict[str, threading.Lock] = {}
|
|
488
|
+
self._lock = threading.Lock()
|
|
489
|
+
|
|
490
|
+
def _get_loading_lock(self, key: str) -> threading.Lock:
|
|
491
|
+
"""Get or create a loading lock for a catalog."""
|
|
492
|
+
with self._lock:
|
|
493
|
+
if key not in self._loading_locks:
|
|
494
|
+
self._loading_locks[key] = threading.Lock()
|
|
495
|
+
return self._loading_locks[key]
|
|
496
|
+
|
|
497
|
+
def _do_load(
|
|
498
|
+
self,
|
|
499
|
+
locale: LocaleInfo,
|
|
500
|
+
namespace: str | None = None,
|
|
501
|
+
) -> dict[str, str]:
|
|
502
|
+
"""Load catalog from storage."""
|
|
503
|
+
return self.storage.load(locale, namespace)
|
|
504
|
+
|
|
505
|
+
def get_catalog(
|
|
506
|
+
self,
|
|
507
|
+
locale: LocaleInfo,
|
|
508
|
+
namespace: str | None = None,
|
|
509
|
+
with_fallback: bool = True,
|
|
510
|
+
) -> dict[str, str]:
|
|
511
|
+
"""Get catalog for a locale.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
locale: Target locale
|
|
515
|
+
namespace: Optional namespace
|
|
516
|
+
with_fallback: Include fallback locale messages
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Message dictionary
|
|
520
|
+
"""
|
|
521
|
+
cache_key = f"{locale.tag}:{namespace or 'default'}"
|
|
522
|
+
|
|
523
|
+
# Check cache
|
|
524
|
+
cached = self._cache.get(cache_key)
|
|
525
|
+
if cached is not None:
|
|
526
|
+
return cached
|
|
527
|
+
|
|
528
|
+
# Load with lock to prevent duplicate loads
|
|
529
|
+
lock = self._get_loading_lock(cache_key)
|
|
530
|
+
with lock:
|
|
531
|
+
# Double-check after acquiring lock
|
|
532
|
+
cached = self._cache.get(cache_key)
|
|
533
|
+
if cached is not None:
|
|
534
|
+
return cached
|
|
535
|
+
|
|
536
|
+
# Load catalog
|
|
537
|
+
catalog = self._do_load(locale, namespace)
|
|
538
|
+
|
|
539
|
+
# Apply fallback if needed
|
|
540
|
+
if with_fallback and locale != self.fallback_locale:
|
|
541
|
+
fallback = self._do_load(self.fallback_locale, namespace)
|
|
542
|
+
catalog = {**fallback, **catalog}
|
|
543
|
+
|
|
544
|
+
# Cache and return
|
|
545
|
+
self._cache.set(cache_key, catalog)
|
|
546
|
+
return catalog
|
|
547
|
+
|
|
548
|
+
def load(
|
|
549
|
+
self,
|
|
550
|
+
locale: LocaleInfo,
|
|
551
|
+
namespace: str | None = None,
|
|
552
|
+
) -> dict[str, str]:
|
|
553
|
+
"""Load catalog (implementation of BaseCatalogLoader)."""
|
|
554
|
+
return self.get_catalog(locale, namespace)
|
|
555
|
+
|
|
556
|
+
def preload(
|
|
557
|
+
self,
|
|
558
|
+
locales: list[str | LocaleInfo],
|
|
559
|
+
namespaces: list[str] | None = None,
|
|
560
|
+
) -> None:
|
|
561
|
+
"""Preload catalogs for multiple locales.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
locales: List of locales to preload
|
|
565
|
+
namespaces: Optional list of namespaces
|
|
566
|
+
"""
|
|
567
|
+
namespaces = namespaces or [None] # type: ignore
|
|
568
|
+
|
|
569
|
+
for locale in locales:
|
|
570
|
+
if isinstance(locale, str):
|
|
571
|
+
locale = LocaleInfo.parse(locale)
|
|
572
|
+
for namespace in namespaces:
|
|
573
|
+
self.get_catalog(locale, namespace)
|
|
574
|
+
|
|
575
|
+
def reload(
|
|
576
|
+
self,
|
|
577
|
+
locale: LocaleInfo | None = None,
|
|
578
|
+
namespace: str | None = None,
|
|
579
|
+
) -> None:
|
|
580
|
+
"""Reload catalogs from storage.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
locale: Specific locale to reload (or all if None)
|
|
584
|
+
namespace: Specific namespace to reload (or all if None)
|
|
585
|
+
"""
|
|
586
|
+
if locale and namespace:
|
|
587
|
+
cache_key = f"{locale.tag}:{namespace}"
|
|
588
|
+
self._cache.delete(cache_key)
|
|
589
|
+
self.get_catalog(locale, namespace)
|
|
590
|
+
elif locale:
|
|
591
|
+
# Reload all namespaces for locale
|
|
592
|
+
for key in list(self._loaded_catalogs.keys()):
|
|
593
|
+
if key.startswith(locale.tag):
|
|
594
|
+
self._cache.delete(key)
|
|
595
|
+
else:
|
|
596
|
+
# Reload all
|
|
597
|
+
self._cache.clear()
|
|
598
|
+
|
|
599
|
+
def get_available_locales(self) -> list[LocaleInfo]:
|
|
600
|
+
"""Get list of available locales."""
|
|
601
|
+
return self.storage.list_locales()
|
|
602
|
+
|
|
603
|
+
def add_catalog(
|
|
604
|
+
self,
|
|
605
|
+
locale: LocaleInfo,
|
|
606
|
+
catalog: dict[str, str],
|
|
607
|
+
namespace: str | None = None,
|
|
608
|
+
) -> None:
|
|
609
|
+
"""Add a catalog programmatically.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
locale: Target locale
|
|
613
|
+
catalog: Message dictionary
|
|
614
|
+
namespace: Optional namespace
|
|
615
|
+
"""
|
|
616
|
+
self.storage.save(locale, catalog, namespace)
|
|
617
|
+
cache_key = f"{locale.tag}:{namespace or 'default'}"
|
|
618
|
+
self._cache.delete(cache_key)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# ==============================================================================
|
|
622
|
+
# Context-Based Message Resolution
|
|
623
|
+
# ==============================================================================
|
|
624
|
+
|
|
625
|
+
@dataclass
|
|
626
|
+
class ContextualMessage:
|
|
627
|
+
"""Message with context variants.
|
|
628
|
+
|
|
629
|
+
Attributes:
|
|
630
|
+
key: Message key
|
|
631
|
+
default: Default message template
|
|
632
|
+
contexts: Context-specific variants
|
|
633
|
+
"""
|
|
634
|
+
key: str
|
|
635
|
+
default: str
|
|
636
|
+
contexts: dict[MessageContext, str] = field(default_factory=dict)
|
|
637
|
+
|
|
638
|
+
def get(self, context: MessageContext | None = None) -> str:
|
|
639
|
+
"""Get message for context.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
context: Message context
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
Appropriate message template
|
|
646
|
+
"""
|
|
647
|
+
if context and context in self.contexts:
|
|
648
|
+
return self.contexts[context]
|
|
649
|
+
return self.default
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class ContextResolver(MessageResolver):
|
|
653
|
+
"""Context-aware message resolver.
|
|
654
|
+
|
|
655
|
+
Resolves messages considering:
|
|
656
|
+
- User context (formal, informal, technical, etc.)
|
|
657
|
+
- Pluralization
|
|
658
|
+
- Locale fallback
|
|
659
|
+
- Message inheritance
|
|
660
|
+
|
|
661
|
+
Example:
|
|
662
|
+
resolver = ContextResolver(catalog_manager)
|
|
663
|
+
|
|
664
|
+
# Resolve with context
|
|
665
|
+
result = resolver.resolve(
|
|
666
|
+
key="greeting",
|
|
667
|
+
locale=LocaleInfo.parse("ko"),
|
|
668
|
+
context=MessageContext.FORMAL,
|
|
669
|
+
name="김철수",
|
|
670
|
+
)
|
|
671
|
+
# -> "김철수님, 안녕하십니까"
|
|
672
|
+
|
|
673
|
+
# Resolve with plural
|
|
674
|
+
result = resolver.resolve_plural(
|
|
675
|
+
key="file_count",
|
|
676
|
+
count=5,
|
|
677
|
+
locale=LocaleInfo.parse("ru"),
|
|
678
|
+
)
|
|
679
|
+
# -> "5 файлов"
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def __init__(
|
|
683
|
+
self,
|
|
684
|
+
catalog_manager: CatalogManager,
|
|
685
|
+
context_separator: str = "@",
|
|
686
|
+
plural_separator: str = "#",
|
|
687
|
+
) -> None:
|
|
688
|
+
"""Initialize resolver.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
catalog_manager: Catalog manager instance
|
|
692
|
+
context_separator: Separator for context keys (key@formal)
|
|
693
|
+
plural_separator: Separator for plural keys (key#one)
|
|
694
|
+
"""
|
|
695
|
+
self.catalog_manager = catalog_manager
|
|
696
|
+
self.context_separator = context_separator
|
|
697
|
+
self.plural_separator = plural_separator
|
|
698
|
+
|
|
699
|
+
def resolve(
|
|
700
|
+
self,
|
|
701
|
+
key: str,
|
|
702
|
+
locale: LocaleInfo,
|
|
703
|
+
context: MessageContext | None = None,
|
|
704
|
+
**params: Any,
|
|
705
|
+
) -> ResolvedMessage:
|
|
706
|
+
"""Resolve a message with context.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
key: Message key
|
|
710
|
+
locale: Target locale
|
|
711
|
+
context: Message context
|
|
712
|
+
**params: Format parameters
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
Resolved message
|
|
716
|
+
"""
|
|
717
|
+
catalog = self.catalog_manager.get_catalog(locale)
|
|
718
|
+
fallback_used = False
|
|
719
|
+
|
|
720
|
+
# Try context-specific key first
|
|
721
|
+
if context:
|
|
722
|
+
context_key = f"{key}{self.context_separator}{context.value}"
|
|
723
|
+
if context_key in catalog:
|
|
724
|
+
template = catalog[context_key]
|
|
725
|
+
elif key in catalog:
|
|
726
|
+
template = catalog[key]
|
|
727
|
+
fallback_used = True
|
|
728
|
+
else:
|
|
729
|
+
template = f"[{key}]"
|
|
730
|
+
fallback_used = True
|
|
731
|
+
else:
|
|
732
|
+
template = catalog.get(key, f"[{key}]")
|
|
733
|
+
fallback_used = key not in catalog
|
|
734
|
+
|
|
735
|
+
# Format message
|
|
736
|
+
try:
|
|
737
|
+
message = template.format(**params)
|
|
738
|
+
except KeyError as e:
|
|
739
|
+
message = f"{template} (missing: {e})"
|
|
740
|
+
|
|
741
|
+
return ResolvedMessage(
|
|
742
|
+
key=key,
|
|
743
|
+
message=message,
|
|
744
|
+
locale=locale,
|
|
745
|
+
context=context,
|
|
746
|
+
fallback=fallback_used,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
def resolve_plural(
|
|
750
|
+
self,
|
|
751
|
+
key: str,
|
|
752
|
+
count: float | int,
|
|
753
|
+
locale: LocaleInfo,
|
|
754
|
+
context: MessageContext | None = None,
|
|
755
|
+
**params: Any,
|
|
756
|
+
) -> PluralizedMessage:
|
|
757
|
+
"""Resolve a pluralized message.
|
|
758
|
+
|
|
759
|
+
Looks for plural forms using the pattern:
|
|
760
|
+
- key#zero, key#one, key#two, key#few, key#many, key#other
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
key: Base message key
|
|
764
|
+
count: Number for pluralization
|
|
765
|
+
locale: Target locale
|
|
766
|
+
context: Message context
|
|
767
|
+
**params: Additional format parameters
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
Pluralized message
|
|
771
|
+
"""
|
|
772
|
+
catalog = self.catalog_manager.get_catalog(locale)
|
|
773
|
+
category = get_plural_category(count, locale)
|
|
774
|
+
|
|
775
|
+
# Build key variants
|
|
776
|
+
if context:
|
|
777
|
+
base_key = f"{key}{self.context_separator}{context.value}"
|
|
778
|
+
else:
|
|
779
|
+
base_key = key
|
|
780
|
+
|
|
781
|
+
# Try plural forms
|
|
782
|
+
plural_key = f"{base_key}{self.plural_separator}{category.value}"
|
|
783
|
+
if plural_key in catalog:
|
|
784
|
+
template = catalog[plural_key]
|
|
785
|
+
elif f"{base_key}{self.plural_separator}other" in catalog:
|
|
786
|
+
template = catalog[f"{base_key}{self.plural_separator}other"]
|
|
787
|
+
elif base_key in catalog:
|
|
788
|
+
template = catalog[base_key]
|
|
789
|
+
else:
|
|
790
|
+
template = f"[{key}]"
|
|
791
|
+
|
|
792
|
+
# Format with count
|
|
793
|
+
try:
|
|
794
|
+
message = template.format(count=count, **params)
|
|
795
|
+
except KeyError as e:
|
|
796
|
+
message = f"{template} (missing: {e})"
|
|
797
|
+
|
|
798
|
+
return PluralizedMessage(
|
|
799
|
+
message=message,
|
|
800
|
+
count=count,
|
|
801
|
+
category=category,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def get_contextual_message(
|
|
805
|
+
self,
|
|
806
|
+
key: str,
|
|
807
|
+
locale: LocaleInfo,
|
|
808
|
+
) -> ContextualMessage:
|
|
809
|
+
"""Get all context variants for a message.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
key: Message key
|
|
813
|
+
locale: Target locale
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
ContextualMessage with all variants
|
|
817
|
+
"""
|
|
818
|
+
catalog = self.catalog_manager.get_catalog(locale)
|
|
819
|
+
|
|
820
|
+
# Get default
|
|
821
|
+
default = catalog.get(key, f"[{key}]")
|
|
822
|
+
|
|
823
|
+
# Get context variants
|
|
824
|
+
contexts: dict[MessageContext, str] = {}
|
|
825
|
+
for ctx in MessageContext:
|
|
826
|
+
context_key = f"{key}{self.context_separator}{ctx.value}"
|
|
827
|
+
if context_key in catalog:
|
|
828
|
+
contexts[ctx] = catalog[context_key]
|
|
829
|
+
|
|
830
|
+
return ContextualMessage(key=key, default=default, contexts=contexts)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
# ==============================================================================
|
|
834
|
+
# Message Composition
|
|
835
|
+
# ==============================================================================
|
|
836
|
+
|
|
837
|
+
class MessageComposer:
|
|
838
|
+
"""Compose messages from multiple sources.
|
|
839
|
+
|
|
840
|
+
Supports message inheritance and composition patterns.
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
composer = MessageComposer(resolver)
|
|
844
|
+
|
|
845
|
+
# Compose from parts
|
|
846
|
+
message = composer.compose(
|
|
847
|
+
template="Result: {status} - {details}",
|
|
848
|
+
parts={
|
|
849
|
+
"status": ("status.success", {}),
|
|
850
|
+
"details": ("details.count", {"count": 5}),
|
|
851
|
+
},
|
|
852
|
+
locale=LocaleInfo.parse("en"),
|
|
853
|
+
)
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
def __init__(self, resolver: ContextResolver) -> None:
|
|
857
|
+
"""Initialize composer.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
resolver: Context resolver instance
|
|
861
|
+
"""
|
|
862
|
+
self.resolver = resolver
|
|
863
|
+
|
|
864
|
+
def compose(
|
|
865
|
+
self,
|
|
866
|
+
template: str,
|
|
867
|
+
parts: dict[str, tuple[str, dict[str, Any]]],
|
|
868
|
+
locale: LocaleInfo,
|
|
869
|
+
context: MessageContext | None = None,
|
|
870
|
+
) -> str:
|
|
871
|
+
"""Compose a message from parts.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
template: Template with {placeholders}
|
|
875
|
+
parts: Dictionary of placeholder -> (key, params) tuples
|
|
876
|
+
locale: Target locale
|
|
877
|
+
context: Message context
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Composed message
|
|
881
|
+
"""
|
|
882
|
+
resolved_parts = {}
|
|
883
|
+
for name, (key, params) in parts.items():
|
|
884
|
+
result = self.resolver.resolve(key, locale, context, **params)
|
|
885
|
+
resolved_parts[name] = result.message
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
return template.format(**resolved_parts)
|
|
889
|
+
except KeyError as e:
|
|
890
|
+
return f"{template} (missing part: {e})"
|
|
891
|
+
|
|
892
|
+
def compose_list(
|
|
893
|
+
self,
|
|
894
|
+
items: list[tuple[str, dict[str, Any]]],
|
|
895
|
+
locale: LocaleInfo,
|
|
896
|
+
separator: str = ", ",
|
|
897
|
+
final_separator: str | None = None,
|
|
898
|
+
context: MessageContext | None = None,
|
|
899
|
+
) -> str:
|
|
900
|
+
"""Compose a list of messages.
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
items: List of (key, params) tuples
|
|
904
|
+
locale: Target locale
|
|
905
|
+
separator: Item separator
|
|
906
|
+
final_separator: Separator before last item (e.g., " and ")
|
|
907
|
+
context: Message context
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
Composed list string
|
|
911
|
+
"""
|
|
912
|
+
resolved = []
|
|
913
|
+
for key, params in items:
|
|
914
|
+
result = self.resolver.resolve(key, locale, context, **params)
|
|
915
|
+
resolved.append(result.message)
|
|
916
|
+
|
|
917
|
+
if not resolved:
|
|
918
|
+
return ""
|
|
919
|
+
|
|
920
|
+
if len(resolved) == 1:
|
|
921
|
+
return resolved[0]
|
|
922
|
+
|
|
923
|
+
if final_separator:
|
|
924
|
+
return f"{separator.join(resolved[:-1])}{final_separator}{resolved[-1]}"
|
|
925
|
+
return separator.join(resolved)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
# ==============================================================================
|
|
929
|
+
# Global Instances and Factory Functions
|
|
930
|
+
# ==============================================================================
|
|
931
|
+
|
|
932
|
+
_catalog_manager: CatalogManager | None = None
|
|
933
|
+
_context_resolver: ContextResolver | None = None
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def get_catalog_manager(
|
|
937
|
+
base_path: Path | str | None = None,
|
|
938
|
+
**kwargs: Any,
|
|
939
|
+
) -> CatalogManager:
|
|
940
|
+
"""Get or create the global catalog manager.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
base_path: Path for filesystem storage
|
|
944
|
+
**kwargs: Additional configuration
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
CatalogManager instance
|
|
948
|
+
"""
|
|
949
|
+
global _catalog_manager
|
|
950
|
+
|
|
951
|
+
if _catalog_manager is None:
|
|
952
|
+
_catalog_manager = CatalogManager(base_path=base_path, **kwargs)
|
|
953
|
+
|
|
954
|
+
return _catalog_manager
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def get_context_resolver() -> ContextResolver:
|
|
958
|
+
"""Get or create the global context resolver.
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
ContextResolver instance
|
|
962
|
+
"""
|
|
963
|
+
global _context_resolver
|
|
964
|
+
|
|
965
|
+
if _context_resolver is None:
|
|
966
|
+
_context_resolver = ContextResolver(get_catalog_manager())
|
|
967
|
+
|
|
968
|
+
return _context_resolver
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def resolve_message(
|
|
972
|
+
key: str,
|
|
973
|
+
locale: str | LocaleInfo,
|
|
974
|
+
context: MessageContext | None = None,
|
|
975
|
+
**params: Any,
|
|
976
|
+
) -> str:
|
|
977
|
+
"""Resolve a message (convenience function).
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
key: Message key
|
|
981
|
+
locale: Target locale
|
|
982
|
+
context: Message context
|
|
983
|
+
**params: Format parameters
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
Resolved message string
|
|
987
|
+
"""
|
|
988
|
+
if isinstance(locale, str):
|
|
989
|
+
locale = LocaleInfo.parse(locale)
|
|
990
|
+
|
|
991
|
+
resolver = get_context_resolver()
|
|
992
|
+
result = resolver.resolve(key, locale, context, **params)
|
|
993
|
+
return result.message
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def resolve_plural_message(
|
|
997
|
+
key: str,
|
|
998
|
+
count: float | int,
|
|
999
|
+
locale: str | LocaleInfo,
|
|
1000
|
+
context: MessageContext | None = None,
|
|
1001
|
+
**params: Any,
|
|
1002
|
+
) -> str:
|
|
1003
|
+
"""Resolve a pluralized message (convenience function).
|
|
1004
|
+
|
|
1005
|
+
Args:
|
|
1006
|
+
key: Message key
|
|
1007
|
+
count: Number for pluralization
|
|
1008
|
+
locale: Target locale
|
|
1009
|
+
context: Message context
|
|
1010
|
+
**params: Additional format parameters
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
Pluralized message string
|
|
1014
|
+
"""
|
|
1015
|
+
if isinstance(locale, str):
|
|
1016
|
+
locale = LocaleInfo.parse(locale)
|
|
1017
|
+
|
|
1018
|
+
resolver = get_context_resolver()
|
|
1019
|
+
result = resolver.resolve_plural(key, count, locale, context, **params)
|
|
1020
|
+
return result.message
|