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,1162 @@
|
|
|
1
|
+
"""Translation Management System (TMS) Integration.
|
|
2
|
+
|
|
3
|
+
This module provides integration with external translation management systems
|
|
4
|
+
for enterprise-grade internationalization workflows.
|
|
5
|
+
|
|
6
|
+
Supported TMS Providers:
|
|
7
|
+
- Crowdin (crowdin.com)
|
|
8
|
+
- Lokalise (lokalise.com)
|
|
9
|
+
- Phrase (phrase.com, formerly PhraseApp)
|
|
10
|
+
- Transifex (transifex.com)
|
|
11
|
+
- POEditor (poeditor.com)
|
|
12
|
+
|
|
13
|
+
Features:
|
|
14
|
+
- Catalog synchronization (push/pull)
|
|
15
|
+
- Translation status tracking
|
|
16
|
+
- Webhook support for real-time updates
|
|
17
|
+
- Batch operations
|
|
18
|
+
- Rate limiting and retry logic
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from truthound.validators.i18n.tms import (
|
|
22
|
+
CrowdinProvider,
|
|
23
|
+
LokaliseProvider,
|
|
24
|
+
TMSManager,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Create provider
|
|
28
|
+
provider = CrowdinProvider(
|
|
29
|
+
api_key="your-api-key",
|
|
30
|
+
project_id="your-project-id",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Sync translations
|
|
34
|
+
updated = provider.sync_catalog(LocaleInfo.parse("ko"), local_catalog)
|
|
35
|
+
|
|
36
|
+
# Check translation status
|
|
37
|
+
status = provider.get_translation_status(LocaleInfo.parse("ko"))
|
|
38
|
+
print(f"Korean translation: {status['validators']:.1%} complete")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import hashlib
|
|
44
|
+
import hmac
|
|
45
|
+
import json
|
|
46
|
+
import logging
|
|
47
|
+
import threading
|
|
48
|
+
import time
|
|
49
|
+
from abc import ABC, abstractmethod
|
|
50
|
+
from dataclasses import dataclass, field
|
|
51
|
+
from datetime import datetime, timedelta
|
|
52
|
+
from enum import Enum
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any, Callable
|
|
55
|
+
from urllib.parse import urljoin
|
|
56
|
+
|
|
57
|
+
from truthound.validators.i18n.protocols import (
|
|
58
|
+
BaseTranslationService,
|
|
59
|
+
LocaleInfo,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ==============================================================================
|
|
67
|
+
# TMS Configuration and Types
|
|
68
|
+
# ==============================================================================
|
|
69
|
+
|
|
70
|
+
class TMSProvider(str, Enum):
|
|
71
|
+
"""Supported TMS providers."""
|
|
72
|
+
CROWDIN = "crowdin"
|
|
73
|
+
LOKALISE = "lokalise"
|
|
74
|
+
PHRASE = "phrase"
|
|
75
|
+
TRANSIFEX = "transifex"
|
|
76
|
+
POEDITOR = "poeditor"
|
|
77
|
+
CUSTOM = "custom"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class TMSConfig:
|
|
82
|
+
"""Configuration for TMS integration.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
provider: TMS provider type
|
|
86
|
+
api_key: API key or token
|
|
87
|
+
project_id: Project identifier
|
|
88
|
+
base_url: API base URL (optional, for self-hosted)
|
|
89
|
+
source_locale: Source locale for translations
|
|
90
|
+
file_format: File format for uploads (json, xliff, etc.)
|
|
91
|
+
rate_limit: Maximum requests per second
|
|
92
|
+
retry_attempts: Number of retry attempts
|
|
93
|
+
retry_delay: Initial retry delay in seconds
|
|
94
|
+
webhook_secret: Secret for webhook verification
|
|
95
|
+
timeout: Request timeout in seconds
|
|
96
|
+
"""
|
|
97
|
+
provider: TMSProvider
|
|
98
|
+
api_key: str
|
|
99
|
+
project_id: str
|
|
100
|
+
base_url: str | None = None
|
|
101
|
+
source_locale: str = "en"
|
|
102
|
+
file_format: str = "json"
|
|
103
|
+
rate_limit: float = 10.0
|
|
104
|
+
retry_attempts: int = 3
|
|
105
|
+
retry_delay: float = 1.0
|
|
106
|
+
webhook_secret: str | None = None
|
|
107
|
+
timeout: int = 30
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class TranslationKey:
|
|
112
|
+
"""A translation key with metadata.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
key: Unique key identifier
|
|
116
|
+
source: Source text
|
|
117
|
+
context: Context or description
|
|
118
|
+
tags: Tags for categorization
|
|
119
|
+
max_length: Maximum allowed length
|
|
120
|
+
pluralizable: Whether key supports pluralization
|
|
121
|
+
"""
|
|
122
|
+
key: str
|
|
123
|
+
source: str
|
|
124
|
+
context: str = ""
|
|
125
|
+
tags: list[str] = field(default_factory=list)
|
|
126
|
+
max_length: int | None = None
|
|
127
|
+
pluralizable: bool = False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class TranslationEntry:
|
|
132
|
+
"""A translation entry from TMS.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
key: Translation key
|
|
136
|
+
source: Source text
|
|
137
|
+
target: Translated text
|
|
138
|
+
locale: Target locale
|
|
139
|
+
is_reviewed: Whether translation is reviewed
|
|
140
|
+
is_approved: Whether translation is approved
|
|
141
|
+
translator: Translator identifier
|
|
142
|
+
updated_at: Last update time
|
|
143
|
+
"""
|
|
144
|
+
key: str
|
|
145
|
+
source: str
|
|
146
|
+
target: str
|
|
147
|
+
locale: str
|
|
148
|
+
is_reviewed: bool = False
|
|
149
|
+
is_approved: bool = False
|
|
150
|
+
translator: str | None = None
|
|
151
|
+
updated_at: datetime | None = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class TranslationStatus:
|
|
156
|
+
"""Translation status for a locale.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
locale: Target locale
|
|
160
|
+
total_keys: Total number of keys
|
|
161
|
+
translated: Number of translated keys
|
|
162
|
+
reviewed: Number of reviewed keys
|
|
163
|
+
approved: Number of approved keys
|
|
164
|
+
progress: Overall progress percentage
|
|
165
|
+
last_activity: Last activity time
|
|
166
|
+
"""
|
|
167
|
+
locale: str
|
|
168
|
+
total_keys: int = 0
|
|
169
|
+
translated: int = 0
|
|
170
|
+
reviewed: int = 0
|
|
171
|
+
approved: int = 0
|
|
172
|
+
progress: float = 0.0
|
|
173
|
+
last_activity: datetime | None = None
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def completion_rate(self) -> float:
|
|
177
|
+
"""Get completion rate as percentage."""
|
|
178
|
+
if self.total_keys == 0:
|
|
179
|
+
return 0.0
|
|
180
|
+
return self.translated / self.total_keys
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class WebhookEvent:
|
|
185
|
+
"""Webhook event from TMS.
|
|
186
|
+
|
|
187
|
+
Attributes:
|
|
188
|
+
event_type: Type of event
|
|
189
|
+
project_id: Project identifier
|
|
190
|
+
locale: Affected locale
|
|
191
|
+
keys: Affected translation keys
|
|
192
|
+
payload: Raw event payload
|
|
193
|
+
timestamp: Event timestamp
|
|
194
|
+
signature: Event signature for verification
|
|
195
|
+
"""
|
|
196
|
+
event_type: str
|
|
197
|
+
project_id: str
|
|
198
|
+
locale: str | None = None
|
|
199
|
+
keys: list[str] = field(default_factory=list)
|
|
200
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
201
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
202
|
+
signature: str | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ==============================================================================
|
|
206
|
+
# Rate Limiter
|
|
207
|
+
# ==============================================================================
|
|
208
|
+
|
|
209
|
+
class RateLimiter:
|
|
210
|
+
"""Token bucket rate limiter.
|
|
211
|
+
|
|
212
|
+
Implements token bucket algorithm for rate limiting API requests.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, rate: float, burst: int | None = None) -> None:
|
|
216
|
+
"""Initialize rate limiter.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
rate: Tokens per second
|
|
220
|
+
burst: Maximum burst size (default: 2x rate)
|
|
221
|
+
"""
|
|
222
|
+
self.rate = rate
|
|
223
|
+
self.burst = burst or int(rate * 2)
|
|
224
|
+
self.tokens = float(self.burst)
|
|
225
|
+
self.last_update = time.monotonic()
|
|
226
|
+
self._lock = threading.Lock()
|
|
227
|
+
|
|
228
|
+
def acquire(self, tokens: int = 1) -> float:
|
|
229
|
+
"""Acquire tokens, blocking if necessary.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
tokens: Number of tokens to acquire
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Time waited in seconds
|
|
236
|
+
"""
|
|
237
|
+
with self._lock:
|
|
238
|
+
now = time.monotonic()
|
|
239
|
+
elapsed = now - self.last_update
|
|
240
|
+
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
|
|
241
|
+
self.last_update = now
|
|
242
|
+
|
|
243
|
+
if self.tokens >= tokens:
|
|
244
|
+
self.tokens -= tokens
|
|
245
|
+
return 0.0
|
|
246
|
+
|
|
247
|
+
# Need to wait
|
|
248
|
+
wait_time = (tokens - self.tokens) / self.rate
|
|
249
|
+
time.sleep(wait_time)
|
|
250
|
+
|
|
251
|
+
now = time.monotonic()
|
|
252
|
+
elapsed = now - self.last_update
|
|
253
|
+
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
|
|
254
|
+
self.tokens -= tokens
|
|
255
|
+
self.last_update = now
|
|
256
|
+
|
|
257
|
+
return wait_time
|
|
258
|
+
|
|
259
|
+
def try_acquire(self, tokens: int = 1) -> bool:
|
|
260
|
+
"""Try to acquire tokens without blocking.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
tokens: Number of tokens to acquire
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if tokens were acquired
|
|
267
|
+
"""
|
|
268
|
+
with self._lock:
|
|
269
|
+
now = time.monotonic()
|
|
270
|
+
elapsed = now - self.last_update
|
|
271
|
+
self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
|
|
272
|
+
self.last_update = now
|
|
273
|
+
|
|
274
|
+
if self.tokens >= tokens:
|
|
275
|
+
self.tokens -= tokens
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ==============================================================================
|
|
281
|
+
# HTTP Client (Abstract)
|
|
282
|
+
# ==============================================================================
|
|
283
|
+
|
|
284
|
+
class HTTPClient(ABC):
|
|
285
|
+
"""Abstract HTTP client for TMS API calls."""
|
|
286
|
+
|
|
287
|
+
@abstractmethod
|
|
288
|
+
def get(self, url: str, headers: dict | None = None) -> dict[str, Any]:
|
|
289
|
+
"""Make GET request."""
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def post(self, url: str, data: dict | None = None, headers: dict | None = None) -> dict[str, Any]:
|
|
294
|
+
"""Make POST request."""
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
def put(self, url: str, data: dict | None = None, headers: dict | None = None) -> dict[str, Any]:
|
|
299
|
+
"""Make PUT request."""
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
@abstractmethod
|
|
303
|
+
def delete(self, url: str, headers: dict | None = None) -> dict[str, Any]:
|
|
304
|
+
"""Make DELETE request."""
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class MockHTTPClient(HTTPClient):
|
|
309
|
+
"""Mock HTTP client for testing."""
|
|
310
|
+
|
|
311
|
+
def __init__(self) -> None:
|
|
312
|
+
self.responses: dict[str, Any] = {}
|
|
313
|
+
self.requests: list[dict] = []
|
|
314
|
+
|
|
315
|
+
def add_response(self, method: str, url: str, response: dict[str, Any]) -> None:
|
|
316
|
+
"""Add a mock response."""
|
|
317
|
+
key = f"{method}:{url}"
|
|
318
|
+
self.responses[key] = response
|
|
319
|
+
|
|
320
|
+
def get(self, url: str, headers: dict | None = None) -> dict[str, Any]:
|
|
321
|
+
self.requests.append({"method": "GET", "url": url, "headers": headers})
|
|
322
|
+
return self.responses.get(f"GET:{url}", {})
|
|
323
|
+
|
|
324
|
+
def post(self, url: str, data: dict | None = None, headers: dict | None = None) -> dict[str, Any]:
|
|
325
|
+
self.requests.append({"method": "POST", "url": url, "data": data, "headers": headers})
|
|
326
|
+
return self.responses.get(f"POST:{url}", {})
|
|
327
|
+
|
|
328
|
+
def put(self, url: str, data: dict | None = None, headers: dict | None = None) -> dict[str, Any]:
|
|
329
|
+
self.requests.append({"method": "PUT", "url": url, "data": data, "headers": headers})
|
|
330
|
+
return self.responses.get(f"PUT:{url}", {})
|
|
331
|
+
|
|
332
|
+
def delete(self, url: str, headers: dict | None = None) -> dict[str, Any]:
|
|
333
|
+
self.requests.append({"method": "DELETE", "url": url, "headers": headers})
|
|
334
|
+
return self.responses.get(f"DELETE:{url}", {})
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ==============================================================================
|
|
338
|
+
# Base TMS Provider
|
|
339
|
+
# ==============================================================================
|
|
340
|
+
|
|
341
|
+
class BaseTMSProvider(BaseTranslationService, ABC):
|
|
342
|
+
"""Base class for TMS provider implementations.
|
|
343
|
+
|
|
344
|
+
Provides common functionality for all TMS integrations:
|
|
345
|
+
- Rate limiting
|
|
346
|
+
- Retry logic
|
|
347
|
+
- Error handling
|
|
348
|
+
- Webhook verification
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
def __init__(
|
|
352
|
+
self,
|
|
353
|
+
config: TMSConfig,
|
|
354
|
+
http_client: HTTPClient | None = None,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Initialize provider.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
config: TMS configuration
|
|
360
|
+
http_client: HTTP client (for testing)
|
|
361
|
+
"""
|
|
362
|
+
super().__init__(config.api_key, config.project_id)
|
|
363
|
+
self.config = config
|
|
364
|
+
self.http_client = http_client or MockHTTPClient()
|
|
365
|
+
self.rate_limiter = RateLimiter(config.rate_limit)
|
|
366
|
+
|
|
367
|
+
def _make_request(
|
|
368
|
+
self,
|
|
369
|
+
method: str,
|
|
370
|
+
endpoint: str,
|
|
371
|
+
data: dict | None = None,
|
|
372
|
+
headers: dict | None = None,
|
|
373
|
+
) -> dict[str, Any]:
|
|
374
|
+
"""Make an API request with rate limiting and retries.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
method: HTTP method
|
|
378
|
+
endpoint: API endpoint
|
|
379
|
+
data: Request data
|
|
380
|
+
headers: Request headers
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Response data
|
|
384
|
+
"""
|
|
385
|
+
# Rate limit
|
|
386
|
+
self.rate_limiter.acquire()
|
|
387
|
+
|
|
388
|
+
# Build URL
|
|
389
|
+
base_url = self.config.base_url or self._get_default_base_url()
|
|
390
|
+
url = urljoin(base_url, endpoint)
|
|
391
|
+
|
|
392
|
+
# Add auth headers
|
|
393
|
+
headers = headers or {}
|
|
394
|
+
headers.update(self._get_auth_headers())
|
|
395
|
+
|
|
396
|
+
# Retry loop
|
|
397
|
+
last_error = None
|
|
398
|
+
for attempt in range(self.config.retry_attempts):
|
|
399
|
+
try:
|
|
400
|
+
if method == "GET":
|
|
401
|
+
return self.http_client.get(url, headers)
|
|
402
|
+
elif method == "POST":
|
|
403
|
+
return self.http_client.post(url, data, headers)
|
|
404
|
+
elif method == "PUT":
|
|
405
|
+
return self.http_client.put(url, data, headers)
|
|
406
|
+
elif method == "DELETE":
|
|
407
|
+
return self.http_client.delete(url, headers)
|
|
408
|
+
else:
|
|
409
|
+
raise ValueError(f"Unsupported method: {method}")
|
|
410
|
+
except Exception as e:
|
|
411
|
+
last_error = e
|
|
412
|
+
if attempt < self.config.retry_attempts - 1:
|
|
413
|
+
delay = self.config.retry_delay * (2 ** attempt)
|
|
414
|
+
logger.warning(f"Request failed, retrying in {delay}s: {e}")
|
|
415
|
+
time.sleep(delay)
|
|
416
|
+
|
|
417
|
+
raise last_error or Exception("Request failed")
|
|
418
|
+
|
|
419
|
+
@abstractmethod
|
|
420
|
+
def _get_default_base_url(self) -> str:
|
|
421
|
+
"""Get the default API base URL."""
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
@abstractmethod
|
|
425
|
+
def _get_auth_headers(self) -> dict[str, str]:
|
|
426
|
+
"""Get authentication headers."""
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
def verify_webhook(self, payload: bytes, signature: str) -> bool:
|
|
430
|
+
"""Verify webhook signature.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
payload: Raw webhook payload
|
|
434
|
+
signature: Provided signature
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
True if signature is valid
|
|
438
|
+
"""
|
|
439
|
+
if not self.config.webhook_secret:
|
|
440
|
+
logger.warning("No webhook secret configured")
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
expected = hmac.new(
|
|
444
|
+
self.config.webhook_secret.encode(),
|
|
445
|
+
payload,
|
|
446
|
+
hashlib.sha256,
|
|
447
|
+
).hexdigest()
|
|
448
|
+
|
|
449
|
+
return hmac.compare_digest(expected, signature)
|
|
450
|
+
|
|
451
|
+
def parse_webhook(self, payload: dict[str, Any]) -> WebhookEvent:
|
|
452
|
+
"""Parse a webhook payload into an event.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
payload: Webhook payload
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
WebhookEvent
|
|
459
|
+
"""
|
|
460
|
+
return WebhookEvent(
|
|
461
|
+
event_type=payload.get("event", "unknown"),
|
|
462
|
+
project_id=payload.get("project_id", ""),
|
|
463
|
+
locale=payload.get("language"),
|
|
464
|
+
keys=payload.get("keys", []),
|
|
465
|
+
payload=payload,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ==============================================================================
|
|
470
|
+
# Crowdin Provider
|
|
471
|
+
# ==============================================================================
|
|
472
|
+
|
|
473
|
+
class CrowdinProvider(BaseTMSProvider):
|
|
474
|
+
"""Crowdin TMS integration.
|
|
475
|
+
|
|
476
|
+
Crowdin API v2 implementation.
|
|
477
|
+
See: https://developer.crowdin.com/api/v2/
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
def __init__(
|
|
481
|
+
self,
|
|
482
|
+
api_key: str,
|
|
483
|
+
project_id: str,
|
|
484
|
+
organization: str | None = None,
|
|
485
|
+
**kwargs: Any,
|
|
486
|
+
) -> None:
|
|
487
|
+
"""Initialize Crowdin provider.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
api_key: Crowdin API token
|
|
491
|
+
project_id: Crowdin project ID
|
|
492
|
+
organization: Organization domain (for enterprise)
|
|
493
|
+
**kwargs: Additional config options
|
|
494
|
+
"""
|
|
495
|
+
config = TMSConfig(
|
|
496
|
+
provider=TMSProvider.CROWDIN,
|
|
497
|
+
api_key=api_key,
|
|
498
|
+
project_id=project_id,
|
|
499
|
+
base_url=f"https://{organization}.api.crowdin.com" if organization else None,
|
|
500
|
+
**kwargs,
|
|
501
|
+
)
|
|
502
|
+
super().__init__(config)
|
|
503
|
+
self.organization = organization
|
|
504
|
+
|
|
505
|
+
def _get_default_base_url(self) -> str:
|
|
506
|
+
return "https://api.crowdin.com/api/v2/"
|
|
507
|
+
|
|
508
|
+
def _get_auth_headers(self) -> dict[str, str]:
|
|
509
|
+
return {
|
|
510
|
+
"Authorization": f"Bearer {self.config.api_key}",
|
|
511
|
+
"Content-Type": "application/json",
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
def sync_catalog(
|
|
515
|
+
self,
|
|
516
|
+
locale: LocaleInfo,
|
|
517
|
+
catalog: dict[str, str],
|
|
518
|
+
) -> dict[str, str]:
|
|
519
|
+
"""Sync local catalog with Crowdin.
|
|
520
|
+
|
|
521
|
+
Downloads translations for the locale and merges with local catalog.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
locale: Target locale
|
|
525
|
+
catalog: Local message catalog
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Updated catalog with Crowdin translations
|
|
529
|
+
"""
|
|
530
|
+
# Get language ID
|
|
531
|
+
language_id = self._locale_to_crowdin(locale)
|
|
532
|
+
|
|
533
|
+
# Build export request
|
|
534
|
+
endpoint = f"projects/{self.config.project_id}/translations/builds"
|
|
535
|
+
data = {
|
|
536
|
+
"targetLanguageIds": [language_id],
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
# Request translation export
|
|
541
|
+
build_response = self._make_request("POST", endpoint, data)
|
|
542
|
+
build_id = build_response.get("data", {}).get("id")
|
|
543
|
+
|
|
544
|
+
if build_id:
|
|
545
|
+
# Download translations
|
|
546
|
+
download_endpoint = f"projects/{self.config.project_id}/translations/builds/{build_id}/download"
|
|
547
|
+
download_response = self._make_request("GET", download_endpoint)
|
|
548
|
+
|
|
549
|
+
# Parse and merge translations
|
|
550
|
+
translations = self._parse_translations(download_response)
|
|
551
|
+
result = catalog.copy()
|
|
552
|
+
result.update(translations)
|
|
553
|
+
return result
|
|
554
|
+
|
|
555
|
+
except Exception as e:
|
|
556
|
+
logger.error(f"Failed to sync with Crowdin: {e}")
|
|
557
|
+
|
|
558
|
+
return catalog
|
|
559
|
+
|
|
560
|
+
def push_new_keys(
|
|
561
|
+
self,
|
|
562
|
+
keys: list[str],
|
|
563
|
+
source_locale: LocaleInfo,
|
|
564
|
+
source_messages: dict[str, str],
|
|
565
|
+
) -> bool:
|
|
566
|
+
"""Push new keys to Crowdin.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
keys: New translation keys
|
|
570
|
+
source_locale: Source locale
|
|
571
|
+
source_messages: Source message templates
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
True if successful
|
|
575
|
+
"""
|
|
576
|
+
if not keys:
|
|
577
|
+
return True
|
|
578
|
+
|
|
579
|
+
endpoint = f"projects/{self.config.project_id}/strings"
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
for key in keys:
|
|
583
|
+
if key in source_messages:
|
|
584
|
+
data = {
|
|
585
|
+
"text": source_messages[key],
|
|
586
|
+
"identifier": key,
|
|
587
|
+
"context": f"Validator message: {key}",
|
|
588
|
+
}
|
|
589
|
+
self._make_request("POST", endpoint, data)
|
|
590
|
+
|
|
591
|
+
return True
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.error(f"Failed to push keys to Crowdin: {e}")
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
def get_translation_status(
|
|
597
|
+
self,
|
|
598
|
+
locale: LocaleInfo,
|
|
599
|
+
) -> dict[str, float]:
|
|
600
|
+
"""Get translation status from Crowdin.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
locale: Target locale
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Dictionary with completion percentages
|
|
607
|
+
"""
|
|
608
|
+
language_id = self._locale_to_crowdin(locale)
|
|
609
|
+
endpoint = f"projects/{self.config.project_id}/languages/{language_id}/progress"
|
|
610
|
+
|
|
611
|
+
try:
|
|
612
|
+
response = self._make_request("GET", endpoint)
|
|
613
|
+
data = response.get("data", [])
|
|
614
|
+
|
|
615
|
+
# Aggregate by file/namespace
|
|
616
|
+
result: dict[str, float] = {}
|
|
617
|
+
for item in data:
|
|
618
|
+
file_id = item.get("fileId", "default")
|
|
619
|
+
progress = item.get("translationProgress", 0)
|
|
620
|
+
result[str(file_id)] = progress / 100.0
|
|
621
|
+
|
|
622
|
+
# Overall progress
|
|
623
|
+
if data:
|
|
624
|
+
result["overall"] = sum(item.get("translationProgress", 0) for item in data) / len(data) / 100.0
|
|
625
|
+
else:
|
|
626
|
+
result["overall"] = 0.0
|
|
627
|
+
|
|
628
|
+
return result
|
|
629
|
+
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.error(f"Failed to get Crowdin status: {e}")
|
|
632
|
+
return {"overall": 0.0}
|
|
633
|
+
|
|
634
|
+
def _locale_to_crowdin(self, locale: LocaleInfo) -> str:
|
|
635
|
+
"""Convert LocaleInfo to Crowdin language ID."""
|
|
636
|
+
# Crowdin uses different format for some locales
|
|
637
|
+
mapping = {
|
|
638
|
+
"zh-Hans": "zh-CN",
|
|
639
|
+
"zh-Hant": "zh-TW",
|
|
640
|
+
"pt-BR": "pt-BR",
|
|
641
|
+
"pt-PT": "pt-PT",
|
|
642
|
+
}
|
|
643
|
+
return mapping.get(locale.tag, locale.tag)
|
|
644
|
+
|
|
645
|
+
def _parse_translations(self, response: dict) -> dict[str, str]:
|
|
646
|
+
"""Parse Crowdin translation response."""
|
|
647
|
+
# This would parse the actual Crowdin file format
|
|
648
|
+
# For now, return empty dict as placeholder
|
|
649
|
+
return response.get("translations", {})
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# ==============================================================================
|
|
653
|
+
# Lokalise Provider
|
|
654
|
+
# ==============================================================================
|
|
655
|
+
|
|
656
|
+
class LokaliseProvider(BaseTMSProvider):
|
|
657
|
+
"""Lokalise TMS integration.
|
|
658
|
+
|
|
659
|
+
Lokalise API v2 implementation.
|
|
660
|
+
See: https://developers.lokalise.com/reference/
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
def __init__(
|
|
664
|
+
self,
|
|
665
|
+
api_key: str,
|
|
666
|
+
project_id: str,
|
|
667
|
+
**kwargs: Any,
|
|
668
|
+
) -> None:
|
|
669
|
+
"""Initialize Lokalise provider.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
api_key: Lokalise API token
|
|
673
|
+
project_id: Lokalise project ID
|
|
674
|
+
**kwargs: Additional config options
|
|
675
|
+
"""
|
|
676
|
+
config = TMSConfig(
|
|
677
|
+
provider=TMSProvider.LOKALISE,
|
|
678
|
+
api_key=api_key,
|
|
679
|
+
project_id=project_id,
|
|
680
|
+
**kwargs,
|
|
681
|
+
)
|
|
682
|
+
super().__init__(config)
|
|
683
|
+
|
|
684
|
+
def _get_default_base_url(self) -> str:
|
|
685
|
+
return "https://api.lokalise.com/api2/"
|
|
686
|
+
|
|
687
|
+
def _get_auth_headers(self) -> dict[str, str]:
|
|
688
|
+
return {
|
|
689
|
+
"X-Api-Token": self.config.api_key,
|
|
690
|
+
"Content-Type": "application/json",
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
def sync_catalog(
|
|
694
|
+
self,
|
|
695
|
+
locale: LocaleInfo,
|
|
696
|
+
catalog: dict[str, str],
|
|
697
|
+
) -> dict[str, str]:
|
|
698
|
+
"""Sync with Lokalise."""
|
|
699
|
+
# Get keys with translations
|
|
700
|
+
endpoint = f"projects/{self.config.project_id}/keys"
|
|
701
|
+
params = {
|
|
702
|
+
"include_translations": 1,
|
|
703
|
+
"filter_langs": self._locale_to_lokalise(locale),
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
try:
|
|
707
|
+
response = self._make_request("GET", endpoint, params)
|
|
708
|
+
keys = response.get("keys", [])
|
|
709
|
+
|
|
710
|
+
result = catalog.copy()
|
|
711
|
+
for key in keys:
|
|
712
|
+
key_name = key.get("key_name", {})
|
|
713
|
+
if isinstance(key_name, dict):
|
|
714
|
+
key_id = key_name.get("web") or key_name.get("other")
|
|
715
|
+
else:
|
|
716
|
+
key_id = key_name
|
|
717
|
+
|
|
718
|
+
translations = key.get("translations", [])
|
|
719
|
+
for trans in translations:
|
|
720
|
+
if trans.get("language_iso") == self._locale_to_lokalise(locale):
|
|
721
|
+
result[key_id] = trans.get("translation", "")
|
|
722
|
+
|
|
723
|
+
return result
|
|
724
|
+
|
|
725
|
+
except Exception as e:
|
|
726
|
+
logger.error(f"Failed to sync with Lokalise: {e}")
|
|
727
|
+
return catalog
|
|
728
|
+
|
|
729
|
+
def push_new_keys(
|
|
730
|
+
self,
|
|
731
|
+
keys: list[str],
|
|
732
|
+
source_locale: LocaleInfo,
|
|
733
|
+
source_messages: dict[str, str],
|
|
734
|
+
) -> bool:
|
|
735
|
+
"""Push new keys to Lokalise."""
|
|
736
|
+
if not keys:
|
|
737
|
+
return True
|
|
738
|
+
|
|
739
|
+
endpoint = f"projects/{self.config.project_id}/keys"
|
|
740
|
+
lang_iso = self._locale_to_lokalise(source_locale)
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
key_data = []
|
|
744
|
+
for key in keys:
|
|
745
|
+
if key in source_messages:
|
|
746
|
+
key_data.append({
|
|
747
|
+
"key_name": key,
|
|
748
|
+
"platforms": ["web"],
|
|
749
|
+
"translations": [{
|
|
750
|
+
"language_iso": lang_iso,
|
|
751
|
+
"translation": source_messages[key],
|
|
752
|
+
}],
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
if key_data:
|
|
756
|
+
self._make_request("POST", endpoint, {"keys": key_data})
|
|
757
|
+
|
|
758
|
+
return True
|
|
759
|
+
except Exception as e:
|
|
760
|
+
logger.error(f"Failed to push keys to Lokalise: {e}")
|
|
761
|
+
return False
|
|
762
|
+
|
|
763
|
+
def get_translation_status(
|
|
764
|
+
self,
|
|
765
|
+
locale: LocaleInfo,
|
|
766
|
+
) -> dict[str, float]:
|
|
767
|
+
"""Get translation status from Lokalise."""
|
|
768
|
+
endpoint = f"projects/{self.config.project_id}/languages"
|
|
769
|
+
|
|
770
|
+
try:
|
|
771
|
+
response = self._make_request("GET", endpoint)
|
|
772
|
+
languages = response.get("languages", [])
|
|
773
|
+
|
|
774
|
+
target_iso = self._locale_to_lokalise(locale)
|
|
775
|
+
for lang in languages:
|
|
776
|
+
if lang.get("lang_iso") == target_iso:
|
|
777
|
+
progress = lang.get("progress", 0)
|
|
778
|
+
return {
|
|
779
|
+
"overall": progress / 100.0,
|
|
780
|
+
"words_total": lang.get("words_total", 0),
|
|
781
|
+
"words_done": lang.get("words_done", 0),
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {"overall": 0.0}
|
|
785
|
+
|
|
786
|
+
except Exception as e:
|
|
787
|
+
logger.error(f"Failed to get Lokalise status: {e}")
|
|
788
|
+
return {"overall": 0.0}
|
|
789
|
+
|
|
790
|
+
def _locale_to_lokalise(self, locale: LocaleInfo) -> str:
|
|
791
|
+
"""Convert LocaleInfo to Lokalise language ISO."""
|
|
792
|
+
if locale.region:
|
|
793
|
+
return f"{locale.language}_{locale.region}"
|
|
794
|
+
return locale.language
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# ==============================================================================
|
|
798
|
+
# Phrase (PhraseApp) Provider
|
|
799
|
+
# ==============================================================================
|
|
800
|
+
|
|
801
|
+
class PhraseProvider(BaseTMSProvider):
|
|
802
|
+
"""Phrase TMS integration.
|
|
803
|
+
|
|
804
|
+
Phrase Strings API v2 implementation.
|
|
805
|
+
See: https://developers.phrase.com/api/
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
def __init__(
|
|
809
|
+
self,
|
|
810
|
+
api_key: str,
|
|
811
|
+
project_id: str,
|
|
812
|
+
**kwargs: Any,
|
|
813
|
+
) -> None:
|
|
814
|
+
"""Initialize Phrase provider."""
|
|
815
|
+
config = TMSConfig(
|
|
816
|
+
provider=TMSProvider.PHRASE,
|
|
817
|
+
api_key=api_key,
|
|
818
|
+
project_id=project_id,
|
|
819
|
+
**kwargs,
|
|
820
|
+
)
|
|
821
|
+
super().__init__(config)
|
|
822
|
+
|
|
823
|
+
def _get_default_base_url(self) -> str:
|
|
824
|
+
return "https://api.phrase.com/v2/"
|
|
825
|
+
|
|
826
|
+
def _get_auth_headers(self) -> dict[str, str]:
|
|
827
|
+
return {
|
|
828
|
+
"Authorization": f"token {self.config.api_key}",
|
|
829
|
+
"Content-Type": "application/json",
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
def sync_catalog(
|
|
833
|
+
self,
|
|
834
|
+
locale: LocaleInfo,
|
|
835
|
+
catalog: dict[str, str],
|
|
836
|
+
) -> dict[str, str]:
|
|
837
|
+
"""Sync with Phrase."""
|
|
838
|
+
endpoint = f"projects/{self.config.project_id}/locales/{locale.tag}/download"
|
|
839
|
+
params = {"file_format": "nested_json"}
|
|
840
|
+
|
|
841
|
+
try:
|
|
842
|
+
response = self._make_request("GET", endpoint, params)
|
|
843
|
+
result = catalog.copy()
|
|
844
|
+
result.update(self._flatten_translations(response))
|
|
845
|
+
return result
|
|
846
|
+
except Exception as e:
|
|
847
|
+
logger.error(f"Failed to sync with Phrase: {e}")
|
|
848
|
+
return catalog
|
|
849
|
+
|
|
850
|
+
def push_new_keys(
|
|
851
|
+
self,
|
|
852
|
+
keys: list[str],
|
|
853
|
+
source_locale: LocaleInfo,
|
|
854
|
+
source_messages: dict[str, str],
|
|
855
|
+
) -> bool:
|
|
856
|
+
"""Push new keys to Phrase."""
|
|
857
|
+
endpoint = f"projects/{self.config.project_id}/keys"
|
|
858
|
+
|
|
859
|
+
try:
|
|
860
|
+
for key in keys:
|
|
861
|
+
if key in source_messages:
|
|
862
|
+
data = {
|
|
863
|
+
"name": key,
|
|
864
|
+
"description": f"Validator message: {key}",
|
|
865
|
+
"default_translation_content": source_messages[key],
|
|
866
|
+
}
|
|
867
|
+
self._make_request("POST", endpoint, data)
|
|
868
|
+
return True
|
|
869
|
+
except Exception as e:
|
|
870
|
+
logger.error(f"Failed to push keys to Phrase: {e}")
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
def get_translation_status(
|
|
874
|
+
self,
|
|
875
|
+
locale: LocaleInfo,
|
|
876
|
+
) -> dict[str, float]:
|
|
877
|
+
"""Get translation status from Phrase."""
|
|
878
|
+
endpoint = f"projects/{self.config.project_id}/locales/{locale.tag}"
|
|
879
|
+
|
|
880
|
+
try:
|
|
881
|
+
response = self._make_request("GET", endpoint)
|
|
882
|
+
stats = response.get("statistics", {})
|
|
883
|
+
return {
|
|
884
|
+
"overall": stats.get("translations_completed_progress", 0) / 100.0,
|
|
885
|
+
"keys_total": stats.get("keys_total_count", 0),
|
|
886
|
+
"translations_completed": stats.get("translations_completed_count", 0),
|
|
887
|
+
}
|
|
888
|
+
except Exception as e:
|
|
889
|
+
logger.error(f"Failed to get Phrase status: {e}")
|
|
890
|
+
return {"overall": 0.0}
|
|
891
|
+
|
|
892
|
+
def _flatten_translations(self, nested: dict) -> dict[str, str]:
|
|
893
|
+
"""Flatten nested translation structure."""
|
|
894
|
+
result = {}
|
|
895
|
+
|
|
896
|
+
def _flatten(obj: Any, prefix: str = "") -> None:
|
|
897
|
+
if isinstance(obj, dict):
|
|
898
|
+
for k, v in obj.items():
|
|
899
|
+
new_key = f"{prefix}.{k}" if prefix else k
|
|
900
|
+
_flatten(v, new_key)
|
|
901
|
+
else:
|
|
902
|
+
result[prefix] = str(obj)
|
|
903
|
+
|
|
904
|
+
_flatten(nested)
|
|
905
|
+
return result
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
# ==============================================================================
|
|
909
|
+
# TMS Manager
|
|
910
|
+
# ==============================================================================
|
|
911
|
+
|
|
912
|
+
class TMSManager:
|
|
913
|
+
"""Manager for TMS operations.
|
|
914
|
+
|
|
915
|
+
Provides a high-level interface for managing translations across
|
|
916
|
+
multiple TMS providers.
|
|
917
|
+
|
|
918
|
+
Example:
|
|
919
|
+
manager = TMSManager()
|
|
920
|
+
|
|
921
|
+
# Add providers
|
|
922
|
+
manager.add_provider("crowdin", CrowdinProvider(...))
|
|
923
|
+
manager.add_provider("lokalise", LokaliseProvider(...))
|
|
924
|
+
|
|
925
|
+
# Sync translations
|
|
926
|
+
catalog = manager.sync_all(LocaleInfo.parse("ko"), local_catalog)
|
|
927
|
+
|
|
928
|
+
# Push new keys
|
|
929
|
+
manager.push_new_keys(["new.key"], LocaleInfo.parse("en"), source_messages)
|
|
930
|
+
"""
|
|
931
|
+
|
|
932
|
+
def __init__(self) -> None:
|
|
933
|
+
self._providers: dict[str, BaseTMSProvider] = {}
|
|
934
|
+
self._webhooks: dict[str, Callable[[WebhookEvent], None]] = {}
|
|
935
|
+
|
|
936
|
+
def add_provider(self, name: str, provider: BaseTMSProvider) -> None:
|
|
937
|
+
"""Add a TMS provider.
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
name: Provider identifier
|
|
941
|
+
provider: TMS provider instance
|
|
942
|
+
"""
|
|
943
|
+
self._providers[name] = provider
|
|
944
|
+
|
|
945
|
+
def remove_provider(self, name: str) -> None:
|
|
946
|
+
"""Remove a TMS provider.
|
|
947
|
+
|
|
948
|
+
Args:
|
|
949
|
+
name: Provider identifier
|
|
950
|
+
"""
|
|
951
|
+
self._providers.pop(name, None)
|
|
952
|
+
|
|
953
|
+
def get_provider(self, name: str) -> BaseTMSProvider | None:
|
|
954
|
+
"""Get a TMS provider by name.
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
name: Provider identifier
|
|
958
|
+
|
|
959
|
+
Returns:
|
|
960
|
+
Provider instance or None
|
|
961
|
+
"""
|
|
962
|
+
return self._providers.get(name)
|
|
963
|
+
|
|
964
|
+
def sync_catalog(
|
|
965
|
+
self,
|
|
966
|
+
locale: LocaleInfo,
|
|
967
|
+
catalog: dict[str, str],
|
|
968
|
+
provider_name: str | None = None,
|
|
969
|
+
) -> dict[str, str]:
|
|
970
|
+
"""Sync catalog with TMS.
|
|
971
|
+
|
|
972
|
+
Args:
|
|
973
|
+
locale: Target locale
|
|
974
|
+
catalog: Local message catalog
|
|
975
|
+
provider_name: Specific provider (or all if None)
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
Updated catalog
|
|
979
|
+
"""
|
|
980
|
+
result = catalog.copy()
|
|
981
|
+
|
|
982
|
+
providers = (
|
|
983
|
+
[self._providers[provider_name]] if provider_name
|
|
984
|
+
else self._providers.values()
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
for provider in providers:
|
|
988
|
+
try:
|
|
989
|
+
result = provider.sync_catalog(locale, result)
|
|
990
|
+
except Exception as e:
|
|
991
|
+
logger.error(f"Failed to sync with {provider.__class__.__name__}: {e}")
|
|
992
|
+
|
|
993
|
+
return result
|
|
994
|
+
|
|
995
|
+
def push_new_keys(
|
|
996
|
+
self,
|
|
997
|
+
keys: list[str],
|
|
998
|
+
source_locale: LocaleInfo,
|
|
999
|
+
source_messages: dict[str, str],
|
|
1000
|
+
provider_name: str | None = None,
|
|
1001
|
+
) -> bool:
|
|
1002
|
+
"""Push new keys to TMS.
|
|
1003
|
+
|
|
1004
|
+
Args:
|
|
1005
|
+
keys: New translation keys
|
|
1006
|
+
source_locale: Source locale
|
|
1007
|
+
source_messages: Source message templates
|
|
1008
|
+
provider_name: Specific provider (or all if None)
|
|
1009
|
+
|
|
1010
|
+
Returns:
|
|
1011
|
+
True if all pushes succeeded
|
|
1012
|
+
"""
|
|
1013
|
+
providers = (
|
|
1014
|
+
[self._providers[provider_name]] if provider_name
|
|
1015
|
+
else self._providers.values()
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
success = True
|
|
1019
|
+
for provider in providers:
|
|
1020
|
+
try:
|
|
1021
|
+
if not provider.push_new_keys(keys, source_locale, source_messages):
|
|
1022
|
+
success = False
|
|
1023
|
+
except Exception as e:
|
|
1024
|
+
logger.error(f"Failed to push to {provider.__class__.__name__}: {e}")
|
|
1025
|
+
success = False
|
|
1026
|
+
|
|
1027
|
+
return success
|
|
1028
|
+
|
|
1029
|
+
def get_translation_status(
|
|
1030
|
+
self,
|
|
1031
|
+
locale: LocaleInfo,
|
|
1032
|
+
provider_name: str | None = None,
|
|
1033
|
+
) -> dict[str, dict[str, float]]:
|
|
1034
|
+
"""Get translation status from TMS.
|
|
1035
|
+
|
|
1036
|
+
Args:
|
|
1037
|
+
locale: Target locale
|
|
1038
|
+
provider_name: Specific provider (or all if None)
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
Dictionary of provider name -> status dict
|
|
1042
|
+
"""
|
|
1043
|
+
if provider_name:
|
|
1044
|
+
providers = {provider_name: self._providers[provider_name]}
|
|
1045
|
+
else:
|
|
1046
|
+
providers = self._providers
|
|
1047
|
+
|
|
1048
|
+
result = {}
|
|
1049
|
+
for name, provider in providers.items():
|
|
1050
|
+
try:
|
|
1051
|
+
result[name] = provider.get_translation_status(locale)
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
logger.error(f"Failed to get status from {name}: {e}")
|
|
1054
|
+
result[name] = {"overall": 0.0, "error": str(e)}
|
|
1055
|
+
|
|
1056
|
+
return result
|
|
1057
|
+
|
|
1058
|
+
def register_webhook_handler(
|
|
1059
|
+
self,
|
|
1060
|
+
provider_name: str,
|
|
1061
|
+
handler: Callable[[WebhookEvent], None],
|
|
1062
|
+
) -> None:
|
|
1063
|
+
"""Register a webhook event handler.
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
provider_name: Provider identifier
|
|
1067
|
+
handler: Event handler function
|
|
1068
|
+
"""
|
|
1069
|
+
self._webhooks[provider_name] = handler
|
|
1070
|
+
|
|
1071
|
+
def handle_webhook(
|
|
1072
|
+
self,
|
|
1073
|
+
provider_name: str,
|
|
1074
|
+
payload: bytes,
|
|
1075
|
+
signature: str | None = None,
|
|
1076
|
+
) -> bool:
|
|
1077
|
+
"""Handle incoming webhook.
|
|
1078
|
+
|
|
1079
|
+
Args:
|
|
1080
|
+
provider_name: Provider identifier
|
|
1081
|
+
payload: Raw webhook payload
|
|
1082
|
+
signature: Webhook signature
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
True if handled successfully
|
|
1086
|
+
"""
|
|
1087
|
+
provider = self._providers.get(provider_name)
|
|
1088
|
+
if not provider:
|
|
1089
|
+
logger.error(f"Unknown provider: {provider_name}")
|
|
1090
|
+
return False
|
|
1091
|
+
|
|
1092
|
+
# Verify signature if provided
|
|
1093
|
+
if signature and not provider.verify_webhook(payload, signature):
|
|
1094
|
+
logger.error("Invalid webhook signature")
|
|
1095
|
+
return False
|
|
1096
|
+
|
|
1097
|
+
# Parse event
|
|
1098
|
+
try:
|
|
1099
|
+
payload_dict = json.loads(payload)
|
|
1100
|
+
event = provider.parse_webhook(payload_dict)
|
|
1101
|
+
except Exception as e:
|
|
1102
|
+
logger.error(f"Failed to parse webhook: {e}")
|
|
1103
|
+
return False
|
|
1104
|
+
|
|
1105
|
+
# Call handler
|
|
1106
|
+
handler = self._webhooks.get(provider_name)
|
|
1107
|
+
if handler:
|
|
1108
|
+
try:
|
|
1109
|
+
handler(event)
|
|
1110
|
+
return True
|
|
1111
|
+
except Exception as e:
|
|
1112
|
+
logger.error(f"Webhook handler error: {e}")
|
|
1113
|
+
return False
|
|
1114
|
+
|
|
1115
|
+
return True
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
# ==============================================================================
|
|
1119
|
+
# Factory Functions
|
|
1120
|
+
# ==============================================================================
|
|
1121
|
+
|
|
1122
|
+
def create_provider(
|
|
1123
|
+
provider_type: TMSProvider | str,
|
|
1124
|
+
api_key: str,
|
|
1125
|
+
project_id: str,
|
|
1126
|
+
**kwargs: Any,
|
|
1127
|
+
) -> BaseTMSProvider:
|
|
1128
|
+
"""Create a TMS provider.
|
|
1129
|
+
|
|
1130
|
+
Args:
|
|
1131
|
+
provider_type: Provider type
|
|
1132
|
+
api_key: API key
|
|
1133
|
+
project_id: Project ID
|
|
1134
|
+
**kwargs: Provider-specific options
|
|
1135
|
+
|
|
1136
|
+
Returns:
|
|
1137
|
+
TMS provider instance
|
|
1138
|
+
"""
|
|
1139
|
+
if isinstance(provider_type, str):
|
|
1140
|
+
provider_type = TMSProvider(provider_type.lower())
|
|
1141
|
+
|
|
1142
|
+
if provider_type == TMSProvider.CROWDIN:
|
|
1143
|
+
return CrowdinProvider(api_key, project_id, **kwargs)
|
|
1144
|
+
elif provider_type == TMSProvider.LOKALISE:
|
|
1145
|
+
return LokaliseProvider(api_key, project_id, **kwargs)
|
|
1146
|
+
elif provider_type == TMSProvider.PHRASE:
|
|
1147
|
+
return PhraseProvider(api_key, project_id, **kwargs)
|
|
1148
|
+
else:
|
|
1149
|
+
raise ValueError(f"Unsupported provider: {provider_type}")
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
# Global manager instance
|
|
1153
|
+
_tms_manager = TMSManager()
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def get_tms_manager() -> TMSManager:
|
|
1157
|
+
"""Get the global TMS manager.
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
TMSManager instance
|
|
1161
|
+
"""
|
|
1162
|
+
return _tms_manager
|