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,1542 @@
|
|
|
1
|
+
"""Internationalization (i18n) System for Error Messages.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive internationalization system for
|
|
4
|
+
error messages with:
|
|
5
|
+
- Protocol-based message providers
|
|
6
|
+
- Locale management and fallback
|
|
7
|
+
- Message catalogs with YAML/JSON support
|
|
8
|
+
- Placeholder formatting with type safety
|
|
9
|
+
- Error code registry
|
|
10
|
+
- Context-aware message resolution
|
|
11
|
+
|
|
12
|
+
Key features:
|
|
13
|
+
- Lazy loading of message catalogs
|
|
14
|
+
- Hierarchical locale fallback (ko_KR -> ko -> en)
|
|
15
|
+
- Pluralization support
|
|
16
|
+
- Named and positional placeholders
|
|
17
|
+
- Custom formatters for complex types
|
|
18
|
+
- Thread-safe locale management
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
from truthound.profiler.i18n import (
|
|
22
|
+
I18n,
|
|
23
|
+
MessageCode,
|
|
24
|
+
get_message,
|
|
25
|
+
set_locale,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Set locale
|
|
29
|
+
set_locale("ko")
|
|
30
|
+
|
|
31
|
+
# Get localized message
|
|
32
|
+
msg = get_message(MessageCode.ANALYSIS_FAILED, column="email")
|
|
33
|
+
# -> "분석 실패: email"
|
|
34
|
+
|
|
35
|
+
# Or use I18n directly
|
|
36
|
+
i18n = I18n.get_instance()
|
|
37
|
+
msg = i18n.t("error.analysis.failed", column="email")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
import locale
|
|
44
|
+
import logging
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import threading
|
|
48
|
+
from abc import ABC, abstractmethod
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from datetime import datetime, timedelta
|
|
51
|
+
from enum import Enum, auto
|
|
52
|
+
from functools import lru_cache
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from string import Template
|
|
55
|
+
from typing import (
|
|
56
|
+
Any,
|
|
57
|
+
Callable,
|
|
58
|
+
ClassVar,
|
|
59
|
+
Generic,
|
|
60
|
+
Iterator,
|
|
61
|
+
Mapping,
|
|
62
|
+
Protocol,
|
|
63
|
+
Sequence,
|
|
64
|
+
TypeVar,
|
|
65
|
+
runtime_checkable,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger("truthound.i18n")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# =============================================================================
|
|
72
|
+
# Message Codes (Error Code Registry)
|
|
73
|
+
# =============================================================================
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MessageCode(str, Enum):
|
|
77
|
+
"""Standardized message codes for all error types.
|
|
78
|
+
|
|
79
|
+
Using codes ensures consistency and enables reliable internationalization.
|
|
80
|
+
Format: CATEGORY_SUBCATEGORY_DESCRIPTION
|
|
81
|
+
|
|
82
|
+
Categories:
|
|
83
|
+
- ERR: General errors
|
|
84
|
+
- ANALYSIS: Analysis-related errors
|
|
85
|
+
- PATTERN: Pattern matching errors
|
|
86
|
+
- TYPE: Type inference errors
|
|
87
|
+
- IO: Input/output errors
|
|
88
|
+
- MEMORY: Memory errors
|
|
89
|
+
- TIMEOUT: Timeout errors
|
|
90
|
+
- VALIDATION: Validation errors
|
|
91
|
+
- CONFIG: Configuration errors
|
|
92
|
+
- CACHE: Cache-related errors
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# General errors
|
|
96
|
+
ERR_UNKNOWN = "err.unknown"
|
|
97
|
+
ERR_INTERNAL = "err.internal"
|
|
98
|
+
ERR_NOT_IMPLEMENTED = "err.not_implemented"
|
|
99
|
+
|
|
100
|
+
# Analysis errors
|
|
101
|
+
ANALYSIS_FAILED = "analysis.failed"
|
|
102
|
+
ANALYSIS_COLUMN_FAILED = "analysis.column_failed"
|
|
103
|
+
ANALYSIS_TABLE_FAILED = "analysis.table_failed"
|
|
104
|
+
ANALYSIS_EMPTY_DATA = "analysis.empty_data"
|
|
105
|
+
ANALYSIS_SKIPPED = "analysis.skipped"
|
|
106
|
+
|
|
107
|
+
# Pattern errors
|
|
108
|
+
PATTERN_INVALID = "pattern.invalid"
|
|
109
|
+
PATTERN_NOT_FOUND = "pattern.not_found"
|
|
110
|
+
PATTERN_COMPILE_FAILED = "pattern.compile_failed"
|
|
111
|
+
PATTERN_MATCH_FAILED = "pattern.match_failed"
|
|
112
|
+
PATTERN_TOO_SLOW = "pattern.too_slow"
|
|
113
|
+
|
|
114
|
+
# Type inference errors
|
|
115
|
+
TYPE_INFERENCE_FAILED = "type.inference_failed"
|
|
116
|
+
TYPE_AMBIGUOUS = "type.ambiguous"
|
|
117
|
+
TYPE_UNSUPPORTED = "type.unsupported"
|
|
118
|
+
TYPE_CAST_FAILED = "type.cast_failed"
|
|
119
|
+
|
|
120
|
+
# IO errors
|
|
121
|
+
IO_FILE_NOT_FOUND = "io.file_not_found"
|
|
122
|
+
IO_PERMISSION_DENIED = "io.permission_denied"
|
|
123
|
+
IO_READ_FAILED = "io.read_failed"
|
|
124
|
+
IO_WRITE_FAILED = "io.write_failed"
|
|
125
|
+
IO_INVALID_FORMAT = "io.invalid_format"
|
|
126
|
+
IO_ENCODING_ERROR = "io.encoding_error"
|
|
127
|
+
|
|
128
|
+
# Memory errors
|
|
129
|
+
MEMORY_EXCEEDED = "memory.exceeded"
|
|
130
|
+
MEMORY_ALLOCATION_FAILED = "memory.allocation_failed"
|
|
131
|
+
MEMORY_LIMIT_WARNING = "memory.limit_warning"
|
|
132
|
+
|
|
133
|
+
# Timeout errors
|
|
134
|
+
TIMEOUT_EXCEEDED = "timeout.exceeded"
|
|
135
|
+
TIMEOUT_COLUMN = "timeout.column"
|
|
136
|
+
TIMEOUT_OPERATION = "timeout.operation"
|
|
137
|
+
|
|
138
|
+
# Validation errors
|
|
139
|
+
VALIDATION_FAILED = "validation.failed"
|
|
140
|
+
VALIDATION_SCHEMA = "validation.schema"
|
|
141
|
+
VALIDATION_CONSTRAINT = "validation.constraint"
|
|
142
|
+
VALIDATION_REQUIRED = "validation.required"
|
|
143
|
+
VALIDATION_TYPE = "validation.type"
|
|
144
|
+
VALIDATION_RANGE = "validation.range"
|
|
145
|
+
VALIDATION_FORMAT = "validation.format"
|
|
146
|
+
|
|
147
|
+
# Configuration errors
|
|
148
|
+
CONFIG_INVALID = "config.invalid"
|
|
149
|
+
CONFIG_MISSING = "config.missing"
|
|
150
|
+
CONFIG_TYPE_MISMATCH = "config.type_mismatch"
|
|
151
|
+
|
|
152
|
+
# Cache errors
|
|
153
|
+
CACHE_MISS = "cache.miss"
|
|
154
|
+
CACHE_EXPIRED = "cache.expired"
|
|
155
|
+
CACHE_INVALID = "cache.invalid"
|
|
156
|
+
CACHE_CONNECTION_FAILED = "cache.connection_failed"
|
|
157
|
+
|
|
158
|
+
# Progress messages (not errors)
|
|
159
|
+
PROGRESS_START = "progress.start"
|
|
160
|
+
PROGRESS_COLUMN = "progress.column"
|
|
161
|
+
PROGRESS_COMPLETE = "progress.complete"
|
|
162
|
+
PROGRESS_FAILED = "progress.failed"
|
|
163
|
+
|
|
164
|
+
# Validation messages
|
|
165
|
+
RULE_GENERATED = "rule.generated"
|
|
166
|
+
RULE_SKIPPED = "rule.skipped"
|
|
167
|
+
SUITE_GENERATED = "suite.generated"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# Locale Management
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class LocaleInfo:
|
|
176
|
+
"""Information about a locale."""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
code: str,
|
|
181
|
+
name: str = "",
|
|
182
|
+
native_name: str = "",
|
|
183
|
+
direction: str = "ltr",
|
|
184
|
+
):
|
|
185
|
+
"""Initialize locale info.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
code: Locale code (e.g., "ko_KR", "en_US")
|
|
189
|
+
name: English name
|
|
190
|
+
native_name: Native name
|
|
191
|
+
direction: Text direction ("ltr" or "rtl")
|
|
192
|
+
"""
|
|
193
|
+
self.code = code
|
|
194
|
+
self.name = name or code
|
|
195
|
+
self.native_name = native_name or name or code
|
|
196
|
+
self.direction = direction
|
|
197
|
+
|
|
198
|
+
# Parse language and region
|
|
199
|
+
parts = code.replace("-", "_").split("_")
|
|
200
|
+
self.language = parts[0].lower()
|
|
201
|
+
self.region = parts[1].upper() if len(parts) > 1 else ""
|
|
202
|
+
|
|
203
|
+
def __str__(self) -> str:
|
|
204
|
+
return self.code
|
|
205
|
+
|
|
206
|
+
def __repr__(self) -> str:
|
|
207
|
+
return f"LocaleInfo({self.code!r})"
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def from_system(cls) -> "LocaleInfo":
|
|
211
|
+
"""Create LocaleInfo from system locale."""
|
|
212
|
+
try:
|
|
213
|
+
# Try to get locale from environment
|
|
214
|
+
system_locale = os.environ.get("LANG", "").split(".")[0]
|
|
215
|
+
if not system_locale:
|
|
216
|
+
system_locale = os.environ.get("LC_ALL", "").split(".")[0]
|
|
217
|
+
if not system_locale:
|
|
218
|
+
# Fallback to locale module
|
|
219
|
+
try:
|
|
220
|
+
system_locale = locale.getlocale()[0] or "en_US"
|
|
221
|
+
except Exception:
|
|
222
|
+
system_locale = "en_US"
|
|
223
|
+
except Exception:
|
|
224
|
+
system_locale = "en_US"
|
|
225
|
+
return cls(system_locale or "en_US")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Built-in locale definitions
|
|
229
|
+
BUILTIN_LOCALES: dict[str, LocaleInfo] = {
|
|
230
|
+
"en": LocaleInfo("en", "English", "English"),
|
|
231
|
+
"en_US": LocaleInfo("en_US", "English (US)", "English (US)"),
|
|
232
|
+
"en_GB": LocaleInfo("en_GB", "English (UK)", "English (UK)"),
|
|
233
|
+
"ko": LocaleInfo("ko", "Korean", "한국어"),
|
|
234
|
+
"ko_KR": LocaleInfo("ko_KR", "Korean (Korea)", "한국어 (대한민국)"),
|
|
235
|
+
"ja": LocaleInfo("ja", "Japanese", "日本語"),
|
|
236
|
+
"ja_JP": LocaleInfo("ja_JP", "Japanese (Japan)", "日本語 (日本)"),
|
|
237
|
+
"zh": LocaleInfo("zh", "Chinese", "中文"),
|
|
238
|
+
"zh_CN": LocaleInfo("zh_CN", "Chinese (Simplified)", "简体中文"),
|
|
239
|
+
"zh_TW": LocaleInfo("zh_TW", "Chinese (Traditional)", "繁體中文"),
|
|
240
|
+
"de": LocaleInfo("de", "German", "Deutsch"),
|
|
241
|
+
"de_DE": LocaleInfo("de_DE", "German (Germany)", "Deutsch (Deutschland)"),
|
|
242
|
+
"fr": LocaleInfo("fr", "French", "Français"),
|
|
243
|
+
"fr_FR": LocaleInfo("fr_FR", "French (France)", "Français (France)"),
|
|
244
|
+
"es": LocaleInfo("es", "Spanish", "Español"),
|
|
245
|
+
"es_ES": LocaleInfo("es_ES", "Spanish (Spain)", "Español (España)"),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class LocaleManager:
|
|
250
|
+
"""Thread-safe locale management.
|
|
251
|
+
|
|
252
|
+
Manages the current locale and provides fallback chain resolution.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(
|
|
256
|
+
self,
|
|
257
|
+
default_locale: str = "en",
|
|
258
|
+
fallback_locale: str = "en",
|
|
259
|
+
):
|
|
260
|
+
"""Initialize locale manager.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
default_locale: Default locale code
|
|
264
|
+
fallback_locale: Ultimate fallback locale
|
|
265
|
+
"""
|
|
266
|
+
self._default = default_locale
|
|
267
|
+
self._fallback = fallback_locale
|
|
268
|
+
self._current = threading.local()
|
|
269
|
+
self._locales: dict[str, LocaleInfo] = dict(BUILTIN_LOCALES)
|
|
270
|
+
self._lock = threading.RLock()
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def current(self) -> str:
|
|
274
|
+
"""Get current locale for this thread."""
|
|
275
|
+
return getattr(self._current, "locale", self._default)
|
|
276
|
+
|
|
277
|
+
@current.setter
|
|
278
|
+
def current(self, value: str) -> None:
|
|
279
|
+
"""Set current locale for this thread."""
|
|
280
|
+
self._current.locale = value
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def default(self) -> str:
|
|
284
|
+
"""Get default locale."""
|
|
285
|
+
return self._default
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def fallback(self) -> str:
|
|
289
|
+
"""Get fallback locale."""
|
|
290
|
+
return self._fallback
|
|
291
|
+
|
|
292
|
+
def set_locale(self, locale_code: str) -> None:
|
|
293
|
+
"""Set the current locale.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
locale_code: Locale code (e.g., "ko", "ko_KR")
|
|
297
|
+
"""
|
|
298
|
+
self.current = locale_code
|
|
299
|
+
|
|
300
|
+
def get_locale(self) -> str:
|
|
301
|
+
"""Get the current locale."""
|
|
302
|
+
return self.current
|
|
303
|
+
|
|
304
|
+
def get_locale_info(self, locale_code: str | None = None) -> LocaleInfo:
|
|
305
|
+
"""Get locale info.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
locale_code: Locale code (default: current)
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
LocaleInfo for the locale
|
|
312
|
+
"""
|
|
313
|
+
code = locale_code or self.current
|
|
314
|
+
if code in self._locales:
|
|
315
|
+
return self._locales[code]
|
|
316
|
+
return LocaleInfo(code)
|
|
317
|
+
|
|
318
|
+
def get_fallback_chain(self, locale_code: str | None = None) -> list[str]:
|
|
319
|
+
"""Get the fallback chain for a locale.
|
|
320
|
+
|
|
321
|
+
The chain goes from specific to general:
|
|
322
|
+
ko_KR -> ko -> en (fallback)
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
locale_code: Starting locale (default: current)
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List of locale codes to try in order
|
|
329
|
+
"""
|
|
330
|
+
code = locale_code or self.current
|
|
331
|
+
info = self.get_locale_info(code)
|
|
332
|
+
|
|
333
|
+
chain = [code]
|
|
334
|
+
|
|
335
|
+
# Add language without region if different
|
|
336
|
+
if info.region and info.language not in chain:
|
|
337
|
+
chain.append(info.language)
|
|
338
|
+
|
|
339
|
+
# Add default if different
|
|
340
|
+
if self._default not in chain:
|
|
341
|
+
chain.append(self._default)
|
|
342
|
+
|
|
343
|
+
# Add fallback if different
|
|
344
|
+
if self._fallback not in chain:
|
|
345
|
+
chain.append(self._fallback)
|
|
346
|
+
|
|
347
|
+
return chain
|
|
348
|
+
|
|
349
|
+
def register_locale(self, locale_info: LocaleInfo) -> None:
|
|
350
|
+
"""Register a new locale.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
locale_info: Locale information
|
|
354
|
+
"""
|
|
355
|
+
with self._lock:
|
|
356
|
+
self._locales[locale_info.code] = locale_info
|
|
357
|
+
|
|
358
|
+
def list_locales(self) -> list[str]:
|
|
359
|
+
"""List all registered locales."""
|
|
360
|
+
return sorted(self._locales.keys())
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Global locale manager instance
|
|
364
|
+
_locale_manager = LocaleManager()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def set_locale(locale_code: str) -> None:
|
|
368
|
+
"""Set the current locale globally."""
|
|
369
|
+
_locale_manager.set_locale(locale_code)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_locale() -> str:
|
|
373
|
+
"""Get the current locale."""
|
|
374
|
+
return _locale_manager.get_locale()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# =============================================================================
|
|
378
|
+
# Message Catalog
|
|
379
|
+
# =============================================================================
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass
|
|
383
|
+
class MessageEntry:
|
|
384
|
+
"""A single message entry in the catalog.
|
|
385
|
+
|
|
386
|
+
Supports simple strings or complex pluralized forms.
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
key: str
|
|
390
|
+
value: str | dict[str, str] # String or {zero, one, other, ...}
|
|
391
|
+
description: str = ""
|
|
392
|
+
placeholders: tuple[str, ...] = ()
|
|
393
|
+
|
|
394
|
+
def get_form(self, count: int | None = None) -> str:
|
|
395
|
+
"""Get the appropriate form based on count.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
count: Count for pluralization
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Message string
|
|
402
|
+
"""
|
|
403
|
+
if isinstance(self.value, str):
|
|
404
|
+
return self.value
|
|
405
|
+
|
|
406
|
+
# Pluralization
|
|
407
|
+
if count is None:
|
|
408
|
+
return self.value.get("other", list(self.value.values())[0])
|
|
409
|
+
|
|
410
|
+
if count == 0 and "zero" in self.value:
|
|
411
|
+
return self.value["zero"]
|
|
412
|
+
elif count == 1 and "one" in self.value:
|
|
413
|
+
return self.value["one"]
|
|
414
|
+
elif count == 2 and "two" in self.value:
|
|
415
|
+
return self.value["two"]
|
|
416
|
+
else:
|
|
417
|
+
return self.value.get("other", self.value.get("one", ""))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class MessageCatalog:
|
|
421
|
+
"""Catalog of messages for a single locale.
|
|
422
|
+
|
|
423
|
+
Messages are organized hierarchically using dot notation:
|
|
424
|
+
error.analysis.failed -> {error: {analysis: {failed: "..."}}}
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
def __init__(
|
|
428
|
+
self,
|
|
429
|
+
locale_code: str,
|
|
430
|
+
messages: dict[str, Any] | None = None,
|
|
431
|
+
):
|
|
432
|
+
"""Initialize message catalog.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
locale_code: Locale this catalog is for
|
|
436
|
+
messages: Initial messages
|
|
437
|
+
"""
|
|
438
|
+
self.locale_code = locale_code
|
|
439
|
+
self._messages: dict[str, MessageEntry] = {}
|
|
440
|
+
self._loaded = False
|
|
441
|
+
|
|
442
|
+
if messages:
|
|
443
|
+
self._load_dict(messages)
|
|
444
|
+
|
|
445
|
+
def _load_dict(self, data: dict[str, Any], prefix: str = "") -> None:
|
|
446
|
+
"""Load messages from a dictionary.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
data: Message dictionary
|
|
450
|
+
prefix: Key prefix for nested messages
|
|
451
|
+
"""
|
|
452
|
+
for key, value in data.items():
|
|
453
|
+
full_key = f"{prefix}.{key}" if prefix else key
|
|
454
|
+
|
|
455
|
+
if isinstance(value, dict):
|
|
456
|
+
# Check if it's a pluralized form or nested
|
|
457
|
+
if any(k in value for k in ("zero", "one", "two", "other")):
|
|
458
|
+
# Pluralized form
|
|
459
|
+
self._messages[full_key] = MessageEntry(
|
|
460
|
+
key=full_key,
|
|
461
|
+
value=value,
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
# Nested messages
|
|
465
|
+
self._load_dict(value, full_key)
|
|
466
|
+
else:
|
|
467
|
+
# Simple string
|
|
468
|
+
self._messages[full_key] = MessageEntry(
|
|
469
|
+
key=full_key,
|
|
470
|
+
value=str(value),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def get(self, key: str, count: int | None = None) -> str | None:
|
|
474
|
+
"""Get a message by key.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
key: Message key (dot notation)
|
|
478
|
+
count: Count for pluralization
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Message string or None
|
|
482
|
+
"""
|
|
483
|
+
entry = self._messages.get(key)
|
|
484
|
+
if entry:
|
|
485
|
+
return entry.get_form(count)
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
def has(self, key: str) -> bool:
|
|
489
|
+
"""Check if a key exists."""
|
|
490
|
+
return key in self._messages
|
|
491
|
+
|
|
492
|
+
def keys(self) -> list[str]:
|
|
493
|
+
"""Get all message keys."""
|
|
494
|
+
return list(self._messages.keys())
|
|
495
|
+
|
|
496
|
+
def __len__(self) -> int:
|
|
497
|
+
return len(self._messages)
|
|
498
|
+
|
|
499
|
+
def __contains__(self, key: str) -> bool:
|
|
500
|
+
return self.has(key)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# =============================================================================
|
|
504
|
+
# Message Loaders
|
|
505
|
+
# =============================================================================
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@runtime_checkable
|
|
509
|
+
class MessageLoader(Protocol):
|
|
510
|
+
"""Protocol for loading message catalogs."""
|
|
511
|
+
|
|
512
|
+
def load(self, locale_code: str) -> MessageCatalog | None:
|
|
513
|
+
"""Load a message catalog for a locale.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
locale_code: Locale to load
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
MessageCatalog or None if not found
|
|
520
|
+
"""
|
|
521
|
+
...
|
|
522
|
+
|
|
523
|
+
def supports(self, locale_code: str) -> bool:
|
|
524
|
+
"""Check if this loader can load a locale.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
locale_code: Locale to check
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
True if supported
|
|
531
|
+
"""
|
|
532
|
+
...
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class DictMessageLoader:
|
|
536
|
+
"""Loads messages from a dictionary."""
|
|
537
|
+
|
|
538
|
+
def __init__(self, messages: dict[str, dict[str, Any]]):
|
|
539
|
+
"""Initialize with messages.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
messages: Dict mapping locale codes to message dicts
|
|
543
|
+
"""
|
|
544
|
+
self._messages = messages
|
|
545
|
+
|
|
546
|
+
def load(self, locale_code: str) -> MessageCatalog | None:
|
|
547
|
+
if locale_code in self._messages:
|
|
548
|
+
return MessageCatalog(locale_code, self._messages[locale_code])
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
def supports(self, locale_code: str) -> bool:
|
|
552
|
+
return locale_code in self._messages
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class FileMessageLoader:
|
|
556
|
+
"""Loads messages from files (JSON or YAML).
|
|
557
|
+
|
|
558
|
+
File naming convention:
|
|
559
|
+
- messages_{locale}.json (e.g., messages_ko.json)
|
|
560
|
+
- messages_{locale}.yaml (e.g., messages_ko.yaml)
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
def __init__(
|
|
564
|
+
self,
|
|
565
|
+
directory: str | Path,
|
|
566
|
+
filename_pattern: str = "messages_{locale}",
|
|
567
|
+
extensions: tuple[str, ...] = (".json", ".yaml", ".yml"),
|
|
568
|
+
):
|
|
569
|
+
"""Initialize file loader.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
directory: Directory containing message files
|
|
573
|
+
filename_pattern: Filename pattern with {locale} placeholder
|
|
574
|
+
extensions: File extensions to try
|
|
575
|
+
"""
|
|
576
|
+
self.directory = Path(directory)
|
|
577
|
+
self.filename_pattern = filename_pattern
|
|
578
|
+
self.extensions = extensions
|
|
579
|
+
|
|
580
|
+
def _get_file_path(self, locale_code: str) -> Path | None:
|
|
581
|
+
"""Get file path for a locale."""
|
|
582
|
+
base_name = self.filename_pattern.format(locale=locale_code)
|
|
583
|
+
for ext in self.extensions:
|
|
584
|
+
path = self.directory / f"{base_name}{ext}"
|
|
585
|
+
if path.exists():
|
|
586
|
+
return path
|
|
587
|
+
return None
|
|
588
|
+
|
|
589
|
+
def load(self, locale_code: str) -> MessageCatalog | None:
|
|
590
|
+
path = self._get_file_path(locale_code)
|
|
591
|
+
if not path:
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
if path.suffix == ".json":
|
|
596
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
597
|
+
data = json.load(f)
|
|
598
|
+
elif path.suffix in (".yaml", ".yml"):
|
|
599
|
+
try:
|
|
600
|
+
import yaml
|
|
601
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
602
|
+
data = yaml.safe_load(f)
|
|
603
|
+
except ImportError:
|
|
604
|
+
logger.warning("YAML support requires PyYAML package")
|
|
605
|
+
return None
|
|
606
|
+
else:
|
|
607
|
+
return None
|
|
608
|
+
|
|
609
|
+
return MessageCatalog(locale_code, data)
|
|
610
|
+
|
|
611
|
+
except Exception as e:
|
|
612
|
+
logger.warning(f"Failed to load messages for {locale_code}: {e}")
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
def supports(self, locale_code: str) -> bool:
|
|
616
|
+
return self._get_file_path(locale_code) is not None
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# =============================================================================
|
|
620
|
+
# Message Formatter
|
|
621
|
+
# =============================================================================
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class PlaceholderFormatter:
|
|
625
|
+
"""Formats messages with placeholders.
|
|
626
|
+
|
|
627
|
+
Supports:
|
|
628
|
+
- Named placeholders: {name}, {column}
|
|
629
|
+
- Indexed placeholders: {0}, {1}
|
|
630
|
+
- Format specs: {count:,d}, {ratio:.2%}
|
|
631
|
+
- Custom formatters for complex types
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
def __init__(self):
|
|
635
|
+
self._type_formatters: dict[type, Callable[[Any], str]] = {}
|
|
636
|
+
self._register_defaults()
|
|
637
|
+
|
|
638
|
+
def _register_defaults(self) -> None:
|
|
639
|
+
"""Register default type formatters."""
|
|
640
|
+
self._type_formatters[datetime] = lambda d: d.isoformat()
|
|
641
|
+
self._type_formatters[timedelta] = self._format_timedelta
|
|
642
|
+
self._type_formatters[Path] = str
|
|
643
|
+
|
|
644
|
+
def _format_timedelta(self, td: timedelta) -> str:
|
|
645
|
+
"""Format timedelta as human-readable string."""
|
|
646
|
+
total_seconds = int(td.total_seconds())
|
|
647
|
+
if total_seconds < 60:
|
|
648
|
+
return f"{total_seconds}s"
|
|
649
|
+
elif total_seconds < 3600:
|
|
650
|
+
mins = total_seconds // 60
|
|
651
|
+
secs = total_seconds % 60
|
|
652
|
+
return f"{mins}m {secs}s" if secs else f"{mins}m"
|
|
653
|
+
else:
|
|
654
|
+
hours = total_seconds // 3600
|
|
655
|
+
mins = (total_seconds % 3600) // 60
|
|
656
|
+
return f"{hours}h {mins}m" if mins else f"{hours}h"
|
|
657
|
+
|
|
658
|
+
def register_formatter(
|
|
659
|
+
self,
|
|
660
|
+
type_: type,
|
|
661
|
+
formatter: Callable[[Any], str],
|
|
662
|
+
) -> None:
|
|
663
|
+
"""Register a custom type formatter.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
type_: Type to format
|
|
667
|
+
formatter: Formatter function
|
|
668
|
+
"""
|
|
669
|
+
self._type_formatters[type_] = formatter
|
|
670
|
+
|
|
671
|
+
def format(
|
|
672
|
+
self,
|
|
673
|
+
template: str,
|
|
674
|
+
*args: Any,
|
|
675
|
+
**kwargs: Any,
|
|
676
|
+
) -> str:
|
|
677
|
+
"""Format a template with placeholders.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
template: Template string
|
|
681
|
+
*args: Positional arguments
|
|
682
|
+
**kwargs: Named arguments
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
Formatted string
|
|
686
|
+
"""
|
|
687
|
+
# Pre-process values through type formatters
|
|
688
|
+
processed_kwargs = {}
|
|
689
|
+
for key, value in kwargs.items():
|
|
690
|
+
value_type = type(value)
|
|
691
|
+
if value_type in self._type_formatters:
|
|
692
|
+
processed_kwargs[key] = self._type_formatters[value_type](value)
|
|
693
|
+
else:
|
|
694
|
+
processed_kwargs[key] = value
|
|
695
|
+
|
|
696
|
+
processed_args = []
|
|
697
|
+
for value in args:
|
|
698
|
+
value_type = type(value)
|
|
699
|
+
if value_type in self._type_formatters:
|
|
700
|
+
processed_args.append(self._type_formatters[value_type](value))
|
|
701
|
+
else:
|
|
702
|
+
processed_args.append(value)
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
# Try str.format first
|
|
706
|
+
return template.format(*processed_args, **processed_kwargs)
|
|
707
|
+
except (KeyError, IndexError):
|
|
708
|
+
# Fall back to Template for simpler substitution
|
|
709
|
+
try:
|
|
710
|
+
t = Template(template)
|
|
711
|
+
return t.safe_substitute(processed_kwargs)
|
|
712
|
+
except Exception:
|
|
713
|
+
return template
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
# =============================================================================
|
|
717
|
+
# Main I18n Class
|
|
718
|
+
# =============================================================================
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class I18n:
|
|
722
|
+
"""Main internationalization interface.
|
|
723
|
+
|
|
724
|
+
Provides message resolution with locale fallback and formatting.
|
|
725
|
+
|
|
726
|
+
Example:
|
|
727
|
+
i18n = I18n.get_instance()
|
|
728
|
+
|
|
729
|
+
# Set locale
|
|
730
|
+
i18n.set_locale("ko")
|
|
731
|
+
|
|
732
|
+
# Get message
|
|
733
|
+
msg = i18n.t("error.analysis.failed", column="email")
|
|
734
|
+
|
|
735
|
+
# Or with message code
|
|
736
|
+
msg = i18n.t(MessageCode.ANALYSIS_FAILED, column="email")
|
|
737
|
+
"""
|
|
738
|
+
|
|
739
|
+
_instance: ClassVar["I18n | None"] = None
|
|
740
|
+
_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
741
|
+
|
|
742
|
+
def __init__(
|
|
743
|
+
self,
|
|
744
|
+
locale_manager: LocaleManager | None = None,
|
|
745
|
+
loaders: Sequence[MessageLoader] | None = None,
|
|
746
|
+
formatter: PlaceholderFormatter | None = None,
|
|
747
|
+
):
|
|
748
|
+
"""Initialize I18n.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
locale_manager: Locale manager
|
|
752
|
+
loaders: Message loaders
|
|
753
|
+
formatter: Placeholder formatter
|
|
754
|
+
"""
|
|
755
|
+
self._locale_manager = locale_manager or _locale_manager
|
|
756
|
+
self._loaders: list[MessageLoader] = list(loaders or [])
|
|
757
|
+
self._formatter = formatter or PlaceholderFormatter()
|
|
758
|
+
self._catalogs: dict[str, MessageCatalog] = {}
|
|
759
|
+
self._catalog_lock = threading.RLock()
|
|
760
|
+
|
|
761
|
+
# Add default English messages
|
|
762
|
+
self._add_default_messages()
|
|
763
|
+
|
|
764
|
+
@classmethod
|
|
765
|
+
def get_instance(cls) -> "I18n":
|
|
766
|
+
"""Get singleton instance."""
|
|
767
|
+
if cls._instance is None:
|
|
768
|
+
with cls._lock:
|
|
769
|
+
if cls._instance is None:
|
|
770
|
+
cls._instance = cls()
|
|
771
|
+
return cls._instance
|
|
772
|
+
|
|
773
|
+
@classmethod
|
|
774
|
+
def reset_instance(cls) -> None:
|
|
775
|
+
"""Reset singleton instance (for testing)."""
|
|
776
|
+
with cls._lock:
|
|
777
|
+
cls._instance = None
|
|
778
|
+
|
|
779
|
+
def _add_default_messages(self) -> None:
|
|
780
|
+
"""Add default English messages."""
|
|
781
|
+
default_messages = {
|
|
782
|
+
"en": {
|
|
783
|
+
"err": {
|
|
784
|
+
"unknown": "Unknown error occurred",
|
|
785
|
+
"internal": "Internal error: {message}",
|
|
786
|
+
"not_implemented": "{feature} is not implemented",
|
|
787
|
+
},
|
|
788
|
+
"analysis": {
|
|
789
|
+
"failed": "Analysis failed for column '{column}'",
|
|
790
|
+
"column_failed": "Column analysis failed: {column} - {reason}",
|
|
791
|
+
"table_failed": "Table analysis failed: {table}",
|
|
792
|
+
"empty_data": "Cannot analyze empty dataset",
|
|
793
|
+
"skipped": "Analysis skipped for column '{column}': {reason}",
|
|
794
|
+
},
|
|
795
|
+
"pattern": {
|
|
796
|
+
"invalid": "Invalid pattern: {pattern}",
|
|
797
|
+
"not_found": "Pattern not found: {pattern}",
|
|
798
|
+
"compile_failed": "Failed to compile pattern '{pattern}': {error}",
|
|
799
|
+
"match_failed": "Pattern matching failed for column '{column}'",
|
|
800
|
+
"too_slow": "Pattern matching too slow for '{column}' (>{timeout}s)",
|
|
801
|
+
},
|
|
802
|
+
"type": {
|
|
803
|
+
"inference_failed": "Type inference failed for column '{column}'",
|
|
804
|
+
"ambiguous": "Ambiguous type for column '{column}': {types}",
|
|
805
|
+
"unsupported": "Unsupported data type: {dtype}",
|
|
806
|
+
"cast_failed": "Failed to cast '{value}' to {target_type}",
|
|
807
|
+
},
|
|
808
|
+
"io": {
|
|
809
|
+
"file_not_found": "File not found: {path}",
|
|
810
|
+
"permission_denied": "Permission denied: {path}",
|
|
811
|
+
"read_failed": "Failed to read file: {path}",
|
|
812
|
+
"write_failed": "Failed to write file: {path}",
|
|
813
|
+
"invalid_format": "Invalid file format: {path}",
|
|
814
|
+
"encoding_error": "Encoding error in file: {path}",
|
|
815
|
+
},
|
|
816
|
+
"memory": {
|
|
817
|
+
"exceeded": "Memory limit exceeded: {used} > {limit}",
|
|
818
|
+
"allocation_failed": "Memory allocation failed: {size}",
|
|
819
|
+
"limit_warning": "Approaching memory limit: {usage:.1%}",
|
|
820
|
+
},
|
|
821
|
+
"timeout": {
|
|
822
|
+
"exceeded": "Operation timed out after {seconds}s",
|
|
823
|
+
"column": "Timeout profiling column '{column}' after {seconds}s",
|
|
824
|
+
"operation": "Operation '{operation}' timed out",
|
|
825
|
+
},
|
|
826
|
+
"validation": {
|
|
827
|
+
"failed": "Validation failed: {message}",
|
|
828
|
+
"schema": "Schema validation failed: {field}",
|
|
829
|
+
"constraint": "Constraint violation: {constraint}",
|
|
830
|
+
"required": "Required field missing: {field}",
|
|
831
|
+
"type": "Type mismatch for '{field}': expected {expected}, got {actual}",
|
|
832
|
+
"range": "Value out of range for '{field}': {value} not in [{min}, {max}]",
|
|
833
|
+
"format": "Invalid format for '{field}': {value}",
|
|
834
|
+
},
|
|
835
|
+
"config": {
|
|
836
|
+
"invalid": "Invalid configuration: {message}",
|
|
837
|
+
"missing": "Missing configuration: {key}",
|
|
838
|
+
"type_mismatch": "Configuration type mismatch for '{key}': expected {expected}",
|
|
839
|
+
},
|
|
840
|
+
"cache": {
|
|
841
|
+
"miss": "Cache miss for key: {key}",
|
|
842
|
+
"expired": "Cache entry expired: {key}",
|
|
843
|
+
"invalid": "Invalid cache entry: {key}",
|
|
844
|
+
"connection_failed": "Cache connection failed: {error}",
|
|
845
|
+
},
|
|
846
|
+
"progress": {
|
|
847
|
+
"start": "Starting profiling: {table}",
|
|
848
|
+
"column": "Profiling column: {column} ({progress:.1%})",
|
|
849
|
+
"complete": "Profiling complete in {duration}",
|
|
850
|
+
"failed": "Profiling failed: {error}",
|
|
851
|
+
},
|
|
852
|
+
"rule": {
|
|
853
|
+
"generated": "Generated {count} validation rules",
|
|
854
|
+
"skipped": "Skipped rule generation for '{column}': {reason}",
|
|
855
|
+
},
|
|
856
|
+
"suite": {
|
|
857
|
+
"generated": "Generated validation suite with {rule_count} rules",
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
"ko": {
|
|
861
|
+
"err": {
|
|
862
|
+
"unknown": "알 수 없는 오류가 발생했습니다",
|
|
863
|
+
"internal": "내부 오류: {message}",
|
|
864
|
+
"not_implemented": "{feature} 기능이 구현되지 않았습니다",
|
|
865
|
+
},
|
|
866
|
+
"analysis": {
|
|
867
|
+
"failed": "'{column}' 컬럼 분석에 실패했습니다",
|
|
868
|
+
"column_failed": "컬럼 분석 실패: {column} - {reason}",
|
|
869
|
+
"table_failed": "테이블 분석 실패: {table}",
|
|
870
|
+
"empty_data": "빈 데이터셋은 분석할 수 없습니다",
|
|
871
|
+
"skipped": "'{column}' 컬럼 분석을 건너뜁니다: {reason}",
|
|
872
|
+
},
|
|
873
|
+
"pattern": {
|
|
874
|
+
"invalid": "잘못된 패턴: {pattern}",
|
|
875
|
+
"not_found": "패턴을 찾을 수 없음: {pattern}",
|
|
876
|
+
"compile_failed": "'{pattern}' 패턴 컴파일 실패: {error}",
|
|
877
|
+
"match_failed": "'{column}' 컬럼의 패턴 매칭 실패",
|
|
878
|
+
"too_slow": "'{column}'의 패턴 매칭이 너무 느립니다 (>{timeout}초)",
|
|
879
|
+
},
|
|
880
|
+
"type": {
|
|
881
|
+
"inference_failed": "'{column}' 컬럼의 타입 추론 실패",
|
|
882
|
+
"ambiguous": "'{column}' 컬럼의 타입이 모호함: {types}",
|
|
883
|
+
"unsupported": "지원하지 않는 데이터 타입: {dtype}",
|
|
884
|
+
"cast_failed": "'{value}'을(를) {target_type}으로 변환할 수 없습니다",
|
|
885
|
+
},
|
|
886
|
+
"io": {
|
|
887
|
+
"file_not_found": "파일을 찾을 수 없음: {path}",
|
|
888
|
+
"permission_denied": "접근 권한 없음: {path}",
|
|
889
|
+
"read_failed": "파일 읽기 실패: {path}",
|
|
890
|
+
"write_failed": "파일 쓰기 실패: {path}",
|
|
891
|
+
"invalid_format": "잘못된 파일 형식: {path}",
|
|
892
|
+
"encoding_error": "파일 인코딩 오류: {path}",
|
|
893
|
+
},
|
|
894
|
+
"memory": {
|
|
895
|
+
"exceeded": "메모리 한도 초과: {used} > {limit}",
|
|
896
|
+
"allocation_failed": "메모리 할당 실패: {size}",
|
|
897
|
+
"limit_warning": "메모리 한도에 근접: {usage:.1%}",
|
|
898
|
+
},
|
|
899
|
+
"timeout": {
|
|
900
|
+
"exceeded": "{seconds}초 후 작업 시간 초과",
|
|
901
|
+
"column": "'{column}' 컬럼 프로파일링 시간 초과 ({seconds}초)",
|
|
902
|
+
"operation": "'{operation}' 작업 시간 초과",
|
|
903
|
+
},
|
|
904
|
+
"validation": {
|
|
905
|
+
"failed": "검증 실패: {message}",
|
|
906
|
+
"schema": "스키마 검증 실패: {field}",
|
|
907
|
+
"constraint": "제약 조건 위반: {constraint}",
|
|
908
|
+
"required": "필수 필드 누락: {field}",
|
|
909
|
+
"type": "'{field}'의 타입 불일치: {expected} 예상, {actual} 발견",
|
|
910
|
+
"range": "'{field}' 값이 범위를 벗어남: {value}이(가) [{min}, {max}]에 없음",
|
|
911
|
+
"format": "'{field}'의 형식이 잘못됨: {value}",
|
|
912
|
+
},
|
|
913
|
+
"config": {
|
|
914
|
+
"invalid": "잘못된 구성: {message}",
|
|
915
|
+
"missing": "구성 누락: {key}",
|
|
916
|
+
"type_mismatch": "'{key}'의 구성 타입 불일치: {expected} 예상",
|
|
917
|
+
},
|
|
918
|
+
"cache": {
|
|
919
|
+
"miss": "캐시 미스: {key}",
|
|
920
|
+
"expired": "캐시 만료됨: {key}",
|
|
921
|
+
"invalid": "잘못된 캐시 항목: {key}",
|
|
922
|
+
"connection_failed": "캐시 연결 실패: {error}",
|
|
923
|
+
},
|
|
924
|
+
"progress": {
|
|
925
|
+
"start": "프로파일링 시작: {table}",
|
|
926
|
+
"column": "컬럼 프로파일링 중: {column} ({progress:.1%})",
|
|
927
|
+
"complete": "프로파일링 완료: {duration}",
|
|
928
|
+
"failed": "프로파일링 실패: {error}",
|
|
929
|
+
},
|
|
930
|
+
"rule": {
|
|
931
|
+
"generated": "{count}개의 검증 규칙 생성됨",
|
|
932
|
+
"skipped": "'{column}'의 규칙 생성 건너뜀: {reason}",
|
|
933
|
+
},
|
|
934
|
+
"suite": {
|
|
935
|
+
"generated": "{rule_count}개 규칙으로 검증 스위트 생성됨",
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
"ja": {
|
|
939
|
+
"err": {
|
|
940
|
+
"unknown": "不明なエラーが発生しました",
|
|
941
|
+
"internal": "内部エラー: {message}",
|
|
942
|
+
"not_implemented": "{feature}は実装されていません",
|
|
943
|
+
},
|
|
944
|
+
"analysis": {
|
|
945
|
+
"failed": "カラム'{column}'の分析に失敗しました",
|
|
946
|
+
"column_failed": "カラム分析失敗: {column} - {reason}",
|
|
947
|
+
"table_failed": "テーブル分析失敗: {table}",
|
|
948
|
+
"empty_data": "空のデータセットは分析できません",
|
|
949
|
+
"skipped": "カラム'{column}'の分析をスキップ: {reason}",
|
|
950
|
+
},
|
|
951
|
+
"pattern": {
|
|
952
|
+
"invalid": "無効なパターン: {pattern}",
|
|
953
|
+
"not_found": "パターンが見つかりません: {pattern}",
|
|
954
|
+
"compile_failed": "パターン'{pattern}'のコンパイル失敗: {error}",
|
|
955
|
+
"match_failed": "カラム'{column}'のパターンマッチング失敗",
|
|
956
|
+
"too_slow": "'{column}'のパターンマッチングが遅すぎます (>{timeout}秒)",
|
|
957
|
+
},
|
|
958
|
+
"type": {
|
|
959
|
+
"inference_failed": "カラム'{column}'の型推論失敗",
|
|
960
|
+
"ambiguous": "カラム'{column}'の型が曖昧: {types}",
|
|
961
|
+
"unsupported": "サポートされていないデータ型: {dtype}",
|
|
962
|
+
"cast_failed": "'{value}'を{target_type}に変換できません",
|
|
963
|
+
},
|
|
964
|
+
"io": {
|
|
965
|
+
"file_not_found": "ファイルが見つかりません: {path}",
|
|
966
|
+
"permission_denied": "アクセス権限がありません: {path}",
|
|
967
|
+
"read_failed": "ファイル読み込み失敗: {path}",
|
|
968
|
+
"write_failed": "ファイル書き込み失敗: {path}",
|
|
969
|
+
"invalid_format": "無効なファイル形式: {path}",
|
|
970
|
+
"encoding_error": "ファイルエンコーディングエラー: {path}",
|
|
971
|
+
},
|
|
972
|
+
"memory": {
|
|
973
|
+
"exceeded": "メモリ制限超過: {used} > {limit}",
|
|
974
|
+
"allocation_failed": "メモリ割り当て失敗: {size}",
|
|
975
|
+
"limit_warning": "メモリ制限に近づいています: {usage:.1%}",
|
|
976
|
+
},
|
|
977
|
+
"timeout": {
|
|
978
|
+
"exceeded": "{seconds}秒後にタイムアウト",
|
|
979
|
+
"column": "カラム'{column}'のプロファイリングがタイムアウト ({seconds}秒)",
|
|
980
|
+
"operation": "操作'{operation}'がタイムアウト",
|
|
981
|
+
},
|
|
982
|
+
"validation": {
|
|
983
|
+
"failed": "検証失敗: {message}",
|
|
984
|
+
"schema": "スキーマ検証失敗: {field}",
|
|
985
|
+
"constraint": "制約違反: {constraint}",
|
|
986
|
+
"required": "必須フィールドが不足: {field}",
|
|
987
|
+
"type": "'{field}'の型不一致: {expected}が期待されましたが{actual}でした",
|
|
988
|
+
"range": "'{field}'の値が範囲外: {value}は[{min}, {max}]にありません",
|
|
989
|
+
"format": "'{field}'の形式が無効: {value}",
|
|
990
|
+
},
|
|
991
|
+
"config": {
|
|
992
|
+
"invalid": "無効な設定: {message}",
|
|
993
|
+
"missing": "設定が不足: {key}",
|
|
994
|
+
"type_mismatch": "'{key}'の設定型不一致: {expected}が期待されました",
|
|
995
|
+
},
|
|
996
|
+
"cache": {
|
|
997
|
+
"miss": "キャッシュミス: {key}",
|
|
998
|
+
"expired": "キャッシュ期限切れ: {key}",
|
|
999
|
+
"invalid": "無効なキャッシュエントリ: {key}",
|
|
1000
|
+
"connection_failed": "キャッシュ接続失敗: {error}",
|
|
1001
|
+
},
|
|
1002
|
+
"progress": {
|
|
1003
|
+
"start": "プロファイリング開始: {table}",
|
|
1004
|
+
"column": "カラムプロファイリング中: {column} ({progress:.1%})",
|
|
1005
|
+
"complete": "プロファイリング完了: {duration}",
|
|
1006
|
+
"failed": "プロファイリング失敗: {error}",
|
|
1007
|
+
},
|
|
1008
|
+
"rule": {
|
|
1009
|
+
"generated": "{count}個の検証ルールを生成しました",
|
|
1010
|
+
"skipped": "'{column}'のルール生成をスキップ: {reason}",
|
|
1011
|
+
},
|
|
1012
|
+
"suite": {
|
|
1013
|
+
"generated": "{rule_count}個のルールで検証スイートを生成しました",
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
loader = DictMessageLoader(default_messages)
|
|
1019
|
+
self._loaders.insert(0, loader)
|
|
1020
|
+
|
|
1021
|
+
def add_loader(self, loader: MessageLoader) -> None:
|
|
1022
|
+
"""Add a message loader.
|
|
1023
|
+
|
|
1024
|
+
Args:
|
|
1025
|
+
loader: Message loader to add
|
|
1026
|
+
"""
|
|
1027
|
+
self._loaders.append(loader)
|
|
1028
|
+
|
|
1029
|
+
def set_locale(self, locale_code: str) -> None:
|
|
1030
|
+
"""Set the current locale.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
locale_code: Locale code
|
|
1034
|
+
"""
|
|
1035
|
+
self._locale_manager.set_locale(locale_code)
|
|
1036
|
+
|
|
1037
|
+
def get_locale(self) -> str:
|
|
1038
|
+
"""Get the current locale."""
|
|
1039
|
+
return self._locale_manager.get_locale()
|
|
1040
|
+
|
|
1041
|
+
def _get_catalog(self, locale_code: str) -> MessageCatalog | None:
|
|
1042
|
+
"""Get or load a message catalog.
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
locale_code: Locale code
|
|
1046
|
+
|
|
1047
|
+
Returns:
|
|
1048
|
+
MessageCatalog or None
|
|
1049
|
+
"""
|
|
1050
|
+
with self._catalog_lock:
|
|
1051
|
+
if locale_code in self._catalogs:
|
|
1052
|
+
return self._catalogs[locale_code]
|
|
1053
|
+
|
|
1054
|
+
# Try loaders in order
|
|
1055
|
+
for loader in self._loaders:
|
|
1056
|
+
try:
|
|
1057
|
+
if loader.supports(locale_code):
|
|
1058
|
+
catalog = loader.load(locale_code)
|
|
1059
|
+
if catalog:
|
|
1060
|
+
self._catalogs[locale_code] = catalog
|
|
1061
|
+
return catalog
|
|
1062
|
+
except Exception as e:
|
|
1063
|
+
logger.debug(f"Loader failed for {locale_code}: {e}")
|
|
1064
|
+
|
|
1065
|
+
return None
|
|
1066
|
+
|
|
1067
|
+
def t(
|
|
1068
|
+
self,
|
|
1069
|
+
key: str | MessageCode,
|
|
1070
|
+
*args: Any,
|
|
1071
|
+
count: int | None = None,
|
|
1072
|
+
locale: str | None = None,
|
|
1073
|
+
default: str | None = None,
|
|
1074
|
+
**kwargs: Any,
|
|
1075
|
+
) -> str:
|
|
1076
|
+
"""Translate a message key.
|
|
1077
|
+
|
|
1078
|
+
Args:
|
|
1079
|
+
key: Message key or MessageCode
|
|
1080
|
+
*args: Positional format arguments
|
|
1081
|
+
count: Count for pluralization
|
|
1082
|
+
locale: Override locale
|
|
1083
|
+
default: Default message if key not found
|
|
1084
|
+
**kwargs: Named format arguments
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Translated and formatted message
|
|
1088
|
+
"""
|
|
1089
|
+
# Convert MessageCode to string
|
|
1090
|
+
if isinstance(key, MessageCode):
|
|
1091
|
+
key = key.value
|
|
1092
|
+
|
|
1093
|
+
# Get fallback chain
|
|
1094
|
+
target_locale = locale or self._locale_manager.current
|
|
1095
|
+
fallback_chain = self._locale_manager.get_fallback_chain(target_locale)
|
|
1096
|
+
|
|
1097
|
+
# Try each locale in chain
|
|
1098
|
+
message = None
|
|
1099
|
+
for loc in fallback_chain:
|
|
1100
|
+
catalog = self._get_catalog(loc)
|
|
1101
|
+
if catalog:
|
|
1102
|
+
message = catalog.get(key, count)
|
|
1103
|
+
if message:
|
|
1104
|
+
break
|
|
1105
|
+
|
|
1106
|
+
# Use default if not found
|
|
1107
|
+
if message is None:
|
|
1108
|
+
if default is not None:
|
|
1109
|
+
message = default
|
|
1110
|
+
else:
|
|
1111
|
+
# Return key as fallback
|
|
1112
|
+
logger.warning(f"Message not found: {key} (locale: {target_locale})")
|
|
1113
|
+
return key
|
|
1114
|
+
|
|
1115
|
+
# Format message
|
|
1116
|
+
try:
|
|
1117
|
+
return self._formatter.format(message, *args, **kwargs)
|
|
1118
|
+
except Exception as e:
|
|
1119
|
+
logger.warning(f"Failed to format message '{key}': {e}")
|
|
1120
|
+
return message
|
|
1121
|
+
|
|
1122
|
+
def has(self, key: str | MessageCode, locale: str | None = None) -> bool:
|
|
1123
|
+
"""Check if a message key exists.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
key: Message key or MessageCode
|
|
1127
|
+
locale: Locale to check
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
True if key exists
|
|
1131
|
+
"""
|
|
1132
|
+
if isinstance(key, MessageCode):
|
|
1133
|
+
key = key.value
|
|
1134
|
+
|
|
1135
|
+
target_locale = locale or self._locale_manager.current
|
|
1136
|
+
fallback_chain = self._locale_manager.get_fallback_chain(target_locale)
|
|
1137
|
+
|
|
1138
|
+
for loc in fallback_chain:
|
|
1139
|
+
catalog = self._get_catalog(loc)
|
|
1140
|
+
if catalog and catalog.has(key):
|
|
1141
|
+
return True
|
|
1142
|
+
|
|
1143
|
+
return False
|
|
1144
|
+
|
|
1145
|
+
def list_keys(self, locale: str | None = None) -> list[str]:
|
|
1146
|
+
"""List all available message keys.
|
|
1147
|
+
|
|
1148
|
+
Args:
|
|
1149
|
+
locale: Locale (default: current)
|
|
1150
|
+
|
|
1151
|
+
Returns:
|
|
1152
|
+
List of keys
|
|
1153
|
+
"""
|
|
1154
|
+
target_locale = locale or self._locale_manager.current
|
|
1155
|
+
catalog = self._get_catalog(target_locale)
|
|
1156
|
+
if catalog:
|
|
1157
|
+
return catalog.keys()
|
|
1158
|
+
return []
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
# =============================================================================
|
|
1162
|
+
# Internationalized Error Classes
|
|
1163
|
+
# =============================================================================
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
class I18nError(Exception):
|
|
1167
|
+
"""Base exception with i18n support.
|
|
1168
|
+
|
|
1169
|
+
Automatically translates error messages based on the current locale.
|
|
1170
|
+
"""
|
|
1171
|
+
|
|
1172
|
+
def __init__(
|
|
1173
|
+
self,
|
|
1174
|
+
code: MessageCode,
|
|
1175
|
+
*,
|
|
1176
|
+
default: str | None = None,
|
|
1177
|
+
locale: str | None = None,
|
|
1178
|
+
**kwargs: Any,
|
|
1179
|
+
):
|
|
1180
|
+
"""Initialize i18n error.
|
|
1181
|
+
|
|
1182
|
+
Args:
|
|
1183
|
+
code: Message code
|
|
1184
|
+
default: Default message
|
|
1185
|
+
locale: Override locale
|
|
1186
|
+
**kwargs: Message format arguments
|
|
1187
|
+
"""
|
|
1188
|
+
self.code = code
|
|
1189
|
+
self.locale = locale
|
|
1190
|
+
self.format_args = kwargs
|
|
1191
|
+
self._default = default
|
|
1192
|
+
|
|
1193
|
+
# Get translated message
|
|
1194
|
+
i18n = I18n.get_instance()
|
|
1195
|
+
message = i18n.t(code, locale=locale, default=default, **kwargs)
|
|
1196
|
+
|
|
1197
|
+
super().__init__(message)
|
|
1198
|
+
|
|
1199
|
+
@property
|
|
1200
|
+
def message_code(self) -> str:
|
|
1201
|
+
"""Get the message code."""
|
|
1202
|
+
return self.code.value
|
|
1203
|
+
|
|
1204
|
+
def get_message(self, locale: str | None = None) -> str:
|
|
1205
|
+
"""Get message in a specific locale.
|
|
1206
|
+
|
|
1207
|
+
Args:
|
|
1208
|
+
locale: Target locale
|
|
1209
|
+
|
|
1210
|
+
Returns:
|
|
1211
|
+
Translated message
|
|
1212
|
+
"""
|
|
1213
|
+
i18n = I18n.get_instance()
|
|
1214
|
+
return i18n.t(
|
|
1215
|
+
self.code,
|
|
1216
|
+
locale=locale or self.locale,
|
|
1217
|
+
default=self._default,
|
|
1218
|
+
**self.format_args,
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
class I18nAnalysisError(I18nError):
|
|
1223
|
+
"""Analysis error with i18n support."""
|
|
1224
|
+
|
|
1225
|
+
def __init__(
|
|
1226
|
+
self,
|
|
1227
|
+
code: MessageCode = MessageCode.ANALYSIS_FAILED,
|
|
1228
|
+
**kwargs: Any,
|
|
1229
|
+
):
|
|
1230
|
+
super().__init__(code, **kwargs)
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
class I18nPatternError(I18nError):
|
|
1234
|
+
"""Pattern error with i18n support."""
|
|
1235
|
+
|
|
1236
|
+
def __init__(
|
|
1237
|
+
self,
|
|
1238
|
+
code: MessageCode = MessageCode.PATTERN_INVALID,
|
|
1239
|
+
**kwargs: Any,
|
|
1240
|
+
):
|
|
1241
|
+
super().__init__(code, **kwargs)
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
class I18nTypeError(I18nError):
|
|
1245
|
+
"""Type inference error with i18n support."""
|
|
1246
|
+
|
|
1247
|
+
def __init__(
|
|
1248
|
+
self,
|
|
1249
|
+
code: MessageCode = MessageCode.TYPE_INFERENCE_FAILED,
|
|
1250
|
+
**kwargs: Any,
|
|
1251
|
+
):
|
|
1252
|
+
super().__init__(code, **kwargs)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
class I18nIOError(I18nError):
|
|
1256
|
+
"""IO error with i18n support."""
|
|
1257
|
+
|
|
1258
|
+
def __init__(
|
|
1259
|
+
self,
|
|
1260
|
+
code: MessageCode = MessageCode.IO_READ_FAILED,
|
|
1261
|
+
**kwargs: Any,
|
|
1262
|
+
):
|
|
1263
|
+
super().__init__(code, **kwargs)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
class I18nTimeoutError(I18nError):
|
|
1267
|
+
"""Timeout error with i18n support."""
|
|
1268
|
+
|
|
1269
|
+
def __init__(
|
|
1270
|
+
self,
|
|
1271
|
+
code: MessageCode = MessageCode.TIMEOUT_EXCEEDED,
|
|
1272
|
+
**kwargs: Any,
|
|
1273
|
+
):
|
|
1274
|
+
super().__init__(code, **kwargs)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
class I18nValidationError(I18nError):
|
|
1278
|
+
"""Validation error with i18n support."""
|
|
1279
|
+
|
|
1280
|
+
def __init__(
|
|
1281
|
+
self,
|
|
1282
|
+
code: MessageCode = MessageCode.VALIDATION_FAILED,
|
|
1283
|
+
**kwargs: Any,
|
|
1284
|
+
):
|
|
1285
|
+
super().__init__(code, **kwargs)
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
# =============================================================================
|
|
1289
|
+
# Message Catalog Registry
|
|
1290
|
+
# =============================================================================
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
class MessageCatalogRegistry:
|
|
1294
|
+
"""Registry for message catalogs.
|
|
1295
|
+
|
|
1296
|
+
Allows registration of custom message catalogs at runtime.
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
_instance: ClassVar["MessageCatalogRegistry | None"] = None
|
|
1300
|
+
|
|
1301
|
+
def __init__(self):
|
|
1302
|
+
self._catalogs: dict[str, MessageCatalog] = {}
|
|
1303
|
+
self._loaders: list[MessageLoader] = []
|
|
1304
|
+
self._lock = threading.RLock()
|
|
1305
|
+
|
|
1306
|
+
@classmethod
|
|
1307
|
+
def get_instance(cls) -> "MessageCatalogRegistry":
|
|
1308
|
+
"""Get singleton instance."""
|
|
1309
|
+
if cls._instance is None:
|
|
1310
|
+
cls._instance = cls()
|
|
1311
|
+
return cls._instance
|
|
1312
|
+
|
|
1313
|
+
def register_catalog(
|
|
1314
|
+
self,
|
|
1315
|
+
locale_code: str,
|
|
1316
|
+
catalog: MessageCatalog,
|
|
1317
|
+
) -> None:
|
|
1318
|
+
"""Register a message catalog.
|
|
1319
|
+
|
|
1320
|
+
Args:
|
|
1321
|
+
locale_code: Locale code
|
|
1322
|
+
catalog: Message catalog
|
|
1323
|
+
"""
|
|
1324
|
+
with self._lock:
|
|
1325
|
+
self._catalogs[locale_code] = catalog
|
|
1326
|
+
|
|
1327
|
+
def register_messages(
|
|
1328
|
+
self,
|
|
1329
|
+
locale_code: str,
|
|
1330
|
+
messages: dict[str, Any],
|
|
1331
|
+
) -> None:
|
|
1332
|
+
"""Register messages from a dictionary.
|
|
1333
|
+
|
|
1334
|
+
Args:
|
|
1335
|
+
locale_code: Locale code
|
|
1336
|
+
messages: Messages dictionary
|
|
1337
|
+
"""
|
|
1338
|
+
catalog = MessageCatalog(locale_code, messages)
|
|
1339
|
+
self.register_catalog(locale_code, catalog)
|
|
1340
|
+
|
|
1341
|
+
def register_loader(self, loader: MessageLoader) -> None:
|
|
1342
|
+
"""Register a message loader.
|
|
1343
|
+
|
|
1344
|
+
Args:
|
|
1345
|
+
loader: Message loader
|
|
1346
|
+
"""
|
|
1347
|
+
with self._lock:
|
|
1348
|
+
self._loaders.append(loader)
|
|
1349
|
+
|
|
1350
|
+
def get_catalog(self, locale_code: str) -> MessageCatalog | None:
|
|
1351
|
+
"""Get a catalog by locale.
|
|
1352
|
+
|
|
1353
|
+
Args:
|
|
1354
|
+
locale_code: Locale code
|
|
1355
|
+
|
|
1356
|
+
Returns:
|
|
1357
|
+
MessageCatalog or None
|
|
1358
|
+
"""
|
|
1359
|
+
with self._lock:
|
|
1360
|
+
if locale_code in self._catalogs:
|
|
1361
|
+
return self._catalogs[locale_code]
|
|
1362
|
+
|
|
1363
|
+
for loader in self._loaders:
|
|
1364
|
+
if loader.supports(locale_code):
|
|
1365
|
+
catalog = loader.load(locale_code)
|
|
1366
|
+
if catalog:
|
|
1367
|
+
self._catalogs[locale_code] = catalog
|
|
1368
|
+
return catalog
|
|
1369
|
+
|
|
1370
|
+
return None
|
|
1371
|
+
|
|
1372
|
+
def list_locales(self) -> list[str]:
|
|
1373
|
+
"""List registered locales."""
|
|
1374
|
+
return list(self._catalogs.keys())
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
# =============================================================================
|
|
1378
|
+
# Convenience Functions
|
|
1379
|
+
# =============================================================================
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def get_message(
|
|
1383
|
+
code: MessageCode,
|
|
1384
|
+
*args: Any,
|
|
1385
|
+
locale: str | None = None,
|
|
1386
|
+
**kwargs: Any,
|
|
1387
|
+
) -> str:
|
|
1388
|
+
"""Get a translated message.
|
|
1389
|
+
|
|
1390
|
+
Args:
|
|
1391
|
+
code: Message code
|
|
1392
|
+
*args: Positional format arguments
|
|
1393
|
+
locale: Override locale
|
|
1394
|
+
**kwargs: Named format arguments
|
|
1395
|
+
|
|
1396
|
+
Returns:
|
|
1397
|
+
Translated message
|
|
1398
|
+
"""
|
|
1399
|
+
return I18n.get_instance().t(code, *args, locale=locale, **kwargs)
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def t(
|
|
1403
|
+
key: str | MessageCode,
|
|
1404
|
+
*args: Any,
|
|
1405
|
+
**kwargs: Any,
|
|
1406
|
+
) -> str:
|
|
1407
|
+
"""Shorthand for translate.
|
|
1408
|
+
|
|
1409
|
+
Args:
|
|
1410
|
+
key: Message key or code
|
|
1411
|
+
*args: Format arguments
|
|
1412
|
+
**kwargs: Named arguments
|
|
1413
|
+
|
|
1414
|
+
Returns:
|
|
1415
|
+
Translated message
|
|
1416
|
+
"""
|
|
1417
|
+
return I18n.get_instance().t(key, *args, **kwargs)
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def register_messages(
|
|
1421
|
+
locale_code: str,
|
|
1422
|
+
messages: dict[str, Any],
|
|
1423
|
+
) -> None:
|
|
1424
|
+
"""Register messages for a locale.
|
|
1425
|
+
|
|
1426
|
+
Args:
|
|
1427
|
+
locale_code: Locale code
|
|
1428
|
+
messages: Messages dictionary
|
|
1429
|
+
"""
|
|
1430
|
+
registry = MessageCatalogRegistry.get_instance()
|
|
1431
|
+
registry.register_messages(locale_code, messages)
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def load_messages_from_file(
|
|
1435
|
+
path: str | Path,
|
|
1436
|
+
locale_code: str | None = None,
|
|
1437
|
+
) -> None:
|
|
1438
|
+
"""Load messages from a file.
|
|
1439
|
+
|
|
1440
|
+
Args:
|
|
1441
|
+
path: Path to message file
|
|
1442
|
+
locale_code: Override locale code (default: from filename)
|
|
1443
|
+
"""
|
|
1444
|
+
path = Path(path)
|
|
1445
|
+
|
|
1446
|
+
# Infer locale from filename if not provided
|
|
1447
|
+
if locale_code is None:
|
|
1448
|
+
match = re.match(r"messages_(\w+)\.", path.name)
|
|
1449
|
+
if match:
|
|
1450
|
+
locale_code = match.group(1)
|
|
1451
|
+
else:
|
|
1452
|
+
locale_code = path.stem
|
|
1453
|
+
|
|
1454
|
+
# Load based on extension
|
|
1455
|
+
if path.suffix == ".json":
|
|
1456
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
1457
|
+
messages = json.load(f)
|
|
1458
|
+
elif path.suffix in (".yaml", ".yml"):
|
|
1459
|
+
try:
|
|
1460
|
+
import yaml
|
|
1461
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
1462
|
+
messages = yaml.safe_load(f)
|
|
1463
|
+
except ImportError:
|
|
1464
|
+
raise ImportError("YAML support requires PyYAML package")
|
|
1465
|
+
else:
|
|
1466
|
+
raise ValueError(f"Unsupported file format: {path.suffix}")
|
|
1467
|
+
|
|
1468
|
+
register_messages(locale_code, messages)
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
def create_message_loader(
|
|
1472
|
+
directory: str | Path,
|
|
1473
|
+
pattern: str = "messages_{locale}",
|
|
1474
|
+
) -> FileMessageLoader:
|
|
1475
|
+
"""Create a file message loader.
|
|
1476
|
+
|
|
1477
|
+
Args:
|
|
1478
|
+
directory: Directory with message files
|
|
1479
|
+
pattern: Filename pattern
|
|
1480
|
+
|
|
1481
|
+
Returns:
|
|
1482
|
+
FileMessageLoader
|
|
1483
|
+
"""
|
|
1484
|
+
return FileMessageLoader(directory, pattern)
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
# =============================================================================
|
|
1488
|
+
# Context Manager for Locale
|
|
1489
|
+
# =============================================================================
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
class locale_context:
|
|
1493
|
+
"""Context manager for temporary locale change.
|
|
1494
|
+
|
|
1495
|
+
Example:
|
|
1496
|
+
with locale_context("ko"):
|
|
1497
|
+
msg = get_message(MessageCode.ANALYSIS_FAILED, column="email")
|
|
1498
|
+
# Original locale restored
|
|
1499
|
+
"""
|
|
1500
|
+
|
|
1501
|
+
def __init__(self, locale_code: str):
|
|
1502
|
+
self.locale_code = locale_code
|
|
1503
|
+
self._previous: str | None = None
|
|
1504
|
+
|
|
1505
|
+
def __enter__(self) -> "locale_context":
|
|
1506
|
+
self._previous = get_locale()
|
|
1507
|
+
set_locale(self.locale_code)
|
|
1508
|
+
return self
|
|
1509
|
+
|
|
1510
|
+
def __exit__(self, *args: Any) -> None:
|
|
1511
|
+
if self._previous is not None:
|
|
1512
|
+
set_locale(self._previous)
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
# =============================================================================
|
|
1516
|
+
# Presets
|
|
1517
|
+
# =============================================================================
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
class I18nPresets:
|
|
1521
|
+
"""Pre-configured I18n setups."""
|
|
1522
|
+
|
|
1523
|
+
@staticmethod
|
|
1524
|
+
def minimal() -> I18n:
|
|
1525
|
+
"""Minimal setup with English only."""
|
|
1526
|
+
return I18n()
|
|
1527
|
+
|
|
1528
|
+
@staticmethod
|
|
1529
|
+
def with_file_loader(directory: str | Path) -> I18n:
|
|
1530
|
+
"""Setup with file loader."""
|
|
1531
|
+
loader = FileMessageLoader(directory)
|
|
1532
|
+
i18n = I18n()
|
|
1533
|
+
i18n.add_loader(loader)
|
|
1534
|
+
return i18n
|
|
1535
|
+
|
|
1536
|
+
@staticmethod
|
|
1537
|
+
def auto_detect_locale() -> I18n:
|
|
1538
|
+
"""Setup with auto-detected system locale."""
|
|
1539
|
+
i18n = I18n()
|
|
1540
|
+
locale_info = LocaleInfo.from_system()
|
|
1541
|
+
i18n.set_locale(locale_info.code)
|
|
1542
|
+
return i18n
|