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.
Files changed (877) hide show
  1. truthound/__init__.py +162 -0
  2. truthound/adapters.py +100 -0
  3. truthound/api.py +365 -0
  4. truthound/audit/__init__.py +248 -0
  5. truthound/audit/core.py +967 -0
  6. truthound/audit/filters.py +620 -0
  7. truthound/audit/formatters.py +707 -0
  8. truthound/audit/logger.py +902 -0
  9. truthound/audit/middleware.py +571 -0
  10. truthound/audit/storage.py +1083 -0
  11. truthound/benchmark/__init__.py +123 -0
  12. truthound/benchmark/base.py +757 -0
  13. truthound/benchmark/comparison.py +635 -0
  14. truthound/benchmark/generators.py +706 -0
  15. truthound/benchmark/reporters.py +718 -0
  16. truthound/benchmark/runner.py +635 -0
  17. truthound/benchmark/scenarios.py +712 -0
  18. truthound/cache.py +252 -0
  19. truthound/checkpoint/__init__.py +136 -0
  20. truthound/checkpoint/actions/__init__.py +164 -0
  21. truthound/checkpoint/actions/base.py +324 -0
  22. truthound/checkpoint/actions/custom.py +234 -0
  23. truthound/checkpoint/actions/discord_notify.py +290 -0
  24. truthound/checkpoint/actions/email_notify.py +405 -0
  25. truthound/checkpoint/actions/github_action.py +406 -0
  26. truthound/checkpoint/actions/opsgenie.py +1499 -0
  27. truthound/checkpoint/actions/pagerduty.py +226 -0
  28. truthound/checkpoint/actions/slack_notify.py +233 -0
  29. truthound/checkpoint/actions/store_result.py +249 -0
  30. truthound/checkpoint/actions/teams_notify.py +1570 -0
  31. truthound/checkpoint/actions/telegram_notify.py +419 -0
  32. truthound/checkpoint/actions/update_docs.py +552 -0
  33. truthound/checkpoint/actions/webhook.py +293 -0
  34. truthound/checkpoint/analytics/__init__.py +147 -0
  35. truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
  36. truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
  37. truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
  38. truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
  39. truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
  40. truthound/checkpoint/analytics/analyzers/base.py +270 -0
  41. truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
  42. truthound/checkpoint/analytics/analyzers/trend.py +314 -0
  43. truthound/checkpoint/analytics/models.py +292 -0
  44. truthound/checkpoint/analytics/protocols.py +549 -0
  45. truthound/checkpoint/analytics/service.py +718 -0
  46. truthound/checkpoint/analytics/stores/__init__.py +16 -0
  47. truthound/checkpoint/analytics/stores/base.py +306 -0
  48. truthound/checkpoint/analytics/stores/memory_store.py +353 -0
  49. truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
  50. truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
  51. truthound/checkpoint/async_actions.py +794 -0
  52. truthound/checkpoint/async_base.py +708 -0
  53. truthound/checkpoint/async_checkpoint.py +617 -0
  54. truthound/checkpoint/async_runner.py +639 -0
  55. truthound/checkpoint/checkpoint.py +527 -0
  56. truthound/checkpoint/ci/__init__.py +61 -0
  57. truthound/checkpoint/ci/detector.py +355 -0
  58. truthound/checkpoint/ci/reporter.py +436 -0
  59. truthound/checkpoint/ci/templates.py +454 -0
  60. truthound/checkpoint/circuitbreaker/__init__.py +133 -0
  61. truthound/checkpoint/circuitbreaker/breaker.py +542 -0
  62. truthound/checkpoint/circuitbreaker/core.py +252 -0
  63. truthound/checkpoint/circuitbreaker/detection.py +459 -0
  64. truthound/checkpoint/circuitbreaker/middleware.py +389 -0
  65. truthound/checkpoint/circuitbreaker/registry.py +357 -0
  66. truthound/checkpoint/distributed/__init__.py +139 -0
  67. truthound/checkpoint/distributed/backends/__init__.py +35 -0
  68. truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
  69. truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
  70. truthound/checkpoint/distributed/backends/local_backend.py +397 -0
  71. truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
  72. truthound/checkpoint/distributed/base.py +774 -0
  73. truthound/checkpoint/distributed/orchestrator.py +765 -0
  74. truthound/checkpoint/distributed/protocols.py +842 -0
  75. truthound/checkpoint/distributed/registry.py +449 -0
  76. truthound/checkpoint/idempotency/__init__.py +120 -0
  77. truthound/checkpoint/idempotency/core.py +295 -0
  78. truthound/checkpoint/idempotency/fingerprint.py +454 -0
  79. truthound/checkpoint/idempotency/locking.py +604 -0
  80. truthound/checkpoint/idempotency/service.py +592 -0
  81. truthound/checkpoint/idempotency/stores.py +653 -0
  82. truthound/checkpoint/monitoring/__init__.py +134 -0
  83. truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
  84. truthound/checkpoint/monitoring/aggregators/base.py +372 -0
  85. truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
  86. truthound/checkpoint/monitoring/aggregators/window.py +493 -0
  87. truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
  88. truthound/checkpoint/monitoring/collectors/base.py +257 -0
  89. truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
  90. truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
  91. truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
  92. truthound/checkpoint/monitoring/events.py +410 -0
  93. truthound/checkpoint/monitoring/protocols.py +636 -0
  94. truthound/checkpoint/monitoring/service.py +578 -0
  95. truthound/checkpoint/monitoring/views/__init__.py +17 -0
  96. truthound/checkpoint/monitoring/views/base.py +172 -0
  97. truthound/checkpoint/monitoring/views/queue_view.py +220 -0
  98. truthound/checkpoint/monitoring/views/task_view.py +240 -0
  99. truthound/checkpoint/monitoring/views/worker_view.py +263 -0
  100. truthound/checkpoint/registry.py +337 -0
  101. truthound/checkpoint/runner.py +356 -0
  102. truthound/checkpoint/transaction/__init__.py +133 -0
  103. truthound/checkpoint/transaction/base.py +389 -0
  104. truthound/checkpoint/transaction/compensatable.py +537 -0
  105. truthound/checkpoint/transaction/coordinator.py +576 -0
  106. truthound/checkpoint/transaction/executor.py +622 -0
  107. truthound/checkpoint/transaction/idempotency.py +534 -0
  108. truthound/checkpoint/transaction/saga/__init__.py +143 -0
  109. truthound/checkpoint/transaction/saga/builder.py +584 -0
  110. truthound/checkpoint/transaction/saga/definition.py +515 -0
  111. truthound/checkpoint/transaction/saga/event_store.py +542 -0
  112. truthound/checkpoint/transaction/saga/patterns.py +833 -0
  113. truthound/checkpoint/transaction/saga/runner.py +718 -0
  114. truthound/checkpoint/transaction/saga/state_machine.py +793 -0
  115. truthound/checkpoint/transaction/saga/strategies.py +780 -0
  116. truthound/checkpoint/transaction/saga/testing.py +886 -0
  117. truthound/checkpoint/triggers/__init__.py +58 -0
  118. truthound/checkpoint/triggers/base.py +237 -0
  119. truthound/checkpoint/triggers/event.py +385 -0
  120. truthound/checkpoint/triggers/schedule.py +355 -0
  121. truthound/cli.py +2358 -0
  122. truthound/cli_modules/__init__.py +124 -0
  123. truthound/cli_modules/advanced/__init__.py +45 -0
  124. truthound/cli_modules/advanced/benchmark.py +343 -0
  125. truthound/cli_modules/advanced/docs.py +225 -0
  126. truthound/cli_modules/advanced/lineage.py +209 -0
  127. truthound/cli_modules/advanced/ml.py +320 -0
  128. truthound/cli_modules/advanced/realtime.py +196 -0
  129. truthound/cli_modules/checkpoint/__init__.py +46 -0
  130. truthound/cli_modules/checkpoint/init.py +114 -0
  131. truthound/cli_modules/checkpoint/list.py +71 -0
  132. truthound/cli_modules/checkpoint/run.py +159 -0
  133. truthound/cli_modules/checkpoint/validate.py +67 -0
  134. truthound/cli_modules/common/__init__.py +71 -0
  135. truthound/cli_modules/common/errors.py +414 -0
  136. truthound/cli_modules/common/options.py +419 -0
  137. truthound/cli_modules/common/output.py +507 -0
  138. truthound/cli_modules/common/protocol.py +552 -0
  139. truthound/cli_modules/core/__init__.py +48 -0
  140. truthound/cli_modules/core/check.py +123 -0
  141. truthound/cli_modules/core/compare.py +104 -0
  142. truthound/cli_modules/core/learn.py +57 -0
  143. truthound/cli_modules/core/mask.py +77 -0
  144. truthound/cli_modules/core/profile.py +65 -0
  145. truthound/cli_modules/core/scan.py +61 -0
  146. truthound/cli_modules/profiler/__init__.py +51 -0
  147. truthound/cli_modules/profiler/auto_profile.py +175 -0
  148. truthound/cli_modules/profiler/metadata.py +107 -0
  149. truthound/cli_modules/profiler/suite.py +283 -0
  150. truthound/cli_modules/registry.py +431 -0
  151. truthound/cli_modules/scaffolding/__init__.py +89 -0
  152. truthound/cli_modules/scaffolding/base.py +631 -0
  153. truthound/cli_modules/scaffolding/commands.py +545 -0
  154. truthound/cli_modules/scaffolding/plugins.py +1072 -0
  155. truthound/cli_modules/scaffolding/reporters.py +594 -0
  156. truthound/cli_modules/scaffolding/validators.py +1127 -0
  157. truthound/common/__init__.py +18 -0
  158. truthound/common/resilience/__init__.py +130 -0
  159. truthound/common/resilience/bulkhead.py +266 -0
  160. truthound/common/resilience/circuit_breaker.py +516 -0
  161. truthound/common/resilience/composite.py +332 -0
  162. truthound/common/resilience/config.py +292 -0
  163. truthound/common/resilience/protocols.py +217 -0
  164. truthound/common/resilience/rate_limiter.py +404 -0
  165. truthound/common/resilience/retry.py +341 -0
  166. truthound/datadocs/__init__.py +260 -0
  167. truthound/datadocs/base.py +571 -0
  168. truthound/datadocs/builder.py +761 -0
  169. truthound/datadocs/charts.py +764 -0
  170. truthound/datadocs/dashboard/__init__.py +63 -0
  171. truthound/datadocs/dashboard/app.py +576 -0
  172. truthound/datadocs/dashboard/components.py +584 -0
  173. truthound/datadocs/dashboard/state.py +240 -0
  174. truthound/datadocs/engine/__init__.py +46 -0
  175. truthound/datadocs/engine/context.py +376 -0
  176. truthound/datadocs/engine/pipeline.py +618 -0
  177. truthound/datadocs/engine/registry.py +469 -0
  178. truthound/datadocs/exporters/__init__.py +49 -0
  179. truthound/datadocs/exporters/base.py +198 -0
  180. truthound/datadocs/exporters/html.py +178 -0
  181. truthound/datadocs/exporters/json_exporter.py +253 -0
  182. truthound/datadocs/exporters/markdown.py +284 -0
  183. truthound/datadocs/exporters/pdf.py +392 -0
  184. truthound/datadocs/i18n/__init__.py +86 -0
  185. truthound/datadocs/i18n/catalog.py +960 -0
  186. truthound/datadocs/i18n/formatting.py +505 -0
  187. truthound/datadocs/i18n/loader.py +256 -0
  188. truthound/datadocs/i18n/plurals.py +378 -0
  189. truthound/datadocs/renderers/__init__.py +42 -0
  190. truthound/datadocs/renderers/base.py +401 -0
  191. truthound/datadocs/renderers/custom.py +342 -0
  192. truthound/datadocs/renderers/jinja.py +697 -0
  193. truthound/datadocs/sections.py +736 -0
  194. truthound/datadocs/styles.py +931 -0
  195. truthound/datadocs/themes/__init__.py +101 -0
  196. truthound/datadocs/themes/base.py +336 -0
  197. truthound/datadocs/themes/default.py +417 -0
  198. truthound/datadocs/themes/enterprise.py +419 -0
  199. truthound/datadocs/themes/loader.py +336 -0
  200. truthound/datadocs/themes.py +301 -0
  201. truthound/datadocs/transformers/__init__.py +57 -0
  202. truthound/datadocs/transformers/base.py +268 -0
  203. truthound/datadocs/transformers/enrichers.py +544 -0
  204. truthound/datadocs/transformers/filters.py +447 -0
  205. truthound/datadocs/transformers/i18n.py +468 -0
  206. truthound/datadocs/versioning/__init__.py +62 -0
  207. truthound/datadocs/versioning/diff.py +639 -0
  208. truthound/datadocs/versioning/storage.py +497 -0
  209. truthound/datadocs/versioning/version.py +358 -0
  210. truthound/datasources/__init__.py +223 -0
  211. truthound/datasources/_async_protocols.py +222 -0
  212. truthound/datasources/_protocols.py +159 -0
  213. truthound/datasources/adapters.py +428 -0
  214. truthound/datasources/async_base.py +599 -0
  215. truthound/datasources/async_factory.py +511 -0
  216. truthound/datasources/base.py +516 -0
  217. truthound/datasources/factory.py +433 -0
  218. truthound/datasources/nosql/__init__.py +47 -0
  219. truthound/datasources/nosql/base.py +487 -0
  220. truthound/datasources/nosql/elasticsearch.py +801 -0
  221. truthound/datasources/nosql/mongodb.py +636 -0
  222. truthound/datasources/pandas_optimized.py +582 -0
  223. truthound/datasources/pandas_source.py +216 -0
  224. truthound/datasources/polars_source.py +395 -0
  225. truthound/datasources/spark_source.py +479 -0
  226. truthound/datasources/sql/__init__.py +154 -0
  227. truthound/datasources/sql/base.py +710 -0
  228. truthound/datasources/sql/bigquery.py +410 -0
  229. truthound/datasources/sql/cloud_base.py +199 -0
  230. truthound/datasources/sql/databricks.py +471 -0
  231. truthound/datasources/sql/mysql.py +316 -0
  232. truthound/datasources/sql/oracle.py +427 -0
  233. truthound/datasources/sql/postgresql.py +321 -0
  234. truthound/datasources/sql/redshift.py +479 -0
  235. truthound/datasources/sql/snowflake.py +439 -0
  236. truthound/datasources/sql/sqlite.py +286 -0
  237. truthound/datasources/sql/sqlserver.py +437 -0
  238. truthound/datasources/streaming/__init__.py +47 -0
  239. truthound/datasources/streaming/base.py +350 -0
  240. truthound/datasources/streaming/kafka.py +670 -0
  241. truthound/decorators.py +98 -0
  242. truthound/docs/__init__.py +69 -0
  243. truthound/docs/extractor.py +971 -0
  244. truthound/docs/generator.py +601 -0
  245. truthound/docs/parser.py +1037 -0
  246. truthound/docs/renderer.py +999 -0
  247. truthound/drift/__init__.py +22 -0
  248. truthound/drift/compare.py +189 -0
  249. truthound/drift/detectors.py +464 -0
  250. truthound/drift/report.py +160 -0
  251. truthound/execution/__init__.py +65 -0
  252. truthound/execution/_protocols.py +324 -0
  253. truthound/execution/base.py +576 -0
  254. truthound/execution/distributed/__init__.py +179 -0
  255. truthound/execution/distributed/aggregations.py +731 -0
  256. truthound/execution/distributed/arrow_bridge.py +817 -0
  257. truthound/execution/distributed/base.py +550 -0
  258. truthound/execution/distributed/dask_engine.py +976 -0
  259. truthound/execution/distributed/mixins.py +766 -0
  260. truthound/execution/distributed/protocols.py +756 -0
  261. truthound/execution/distributed/ray_engine.py +1127 -0
  262. truthound/execution/distributed/registry.py +446 -0
  263. truthound/execution/distributed/spark_engine.py +1011 -0
  264. truthound/execution/distributed/validator_adapter.py +682 -0
  265. truthound/execution/pandas_engine.py +401 -0
  266. truthound/execution/polars_engine.py +497 -0
  267. truthound/execution/pushdown/__init__.py +230 -0
  268. truthound/execution/pushdown/ast.py +1550 -0
  269. truthound/execution/pushdown/builder.py +1550 -0
  270. truthound/execution/pushdown/dialects.py +1072 -0
  271. truthound/execution/pushdown/executor.py +829 -0
  272. truthound/execution/pushdown/optimizer.py +1041 -0
  273. truthound/execution/sql_engine.py +518 -0
  274. truthound/infrastructure/__init__.py +189 -0
  275. truthound/infrastructure/audit.py +1515 -0
  276. truthound/infrastructure/config.py +1133 -0
  277. truthound/infrastructure/encryption.py +1132 -0
  278. truthound/infrastructure/logging.py +1503 -0
  279. truthound/infrastructure/metrics.py +1220 -0
  280. truthound/lineage/__init__.py +89 -0
  281. truthound/lineage/base.py +746 -0
  282. truthound/lineage/impact_analysis.py +474 -0
  283. truthound/lineage/integrations/__init__.py +22 -0
  284. truthound/lineage/integrations/openlineage.py +548 -0
  285. truthound/lineage/tracker.py +512 -0
  286. truthound/lineage/visualization/__init__.py +33 -0
  287. truthound/lineage/visualization/protocols.py +145 -0
  288. truthound/lineage/visualization/renderers/__init__.py +20 -0
  289. truthound/lineage/visualization/renderers/cytoscape.py +329 -0
  290. truthound/lineage/visualization/renderers/d3.py +331 -0
  291. truthound/lineage/visualization/renderers/graphviz.py +276 -0
  292. truthound/lineage/visualization/renderers/mermaid.py +308 -0
  293. truthound/maskers.py +113 -0
  294. truthound/ml/__init__.py +124 -0
  295. truthound/ml/anomaly_models/__init__.py +31 -0
  296. truthound/ml/anomaly_models/ensemble.py +362 -0
  297. truthound/ml/anomaly_models/isolation_forest.py +444 -0
  298. truthound/ml/anomaly_models/statistical.py +392 -0
  299. truthound/ml/base.py +1178 -0
  300. truthound/ml/drift_detection/__init__.py +26 -0
  301. truthound/ml/drift_detection/concept.py +381 -0
  302. truthound/ml/drift_detection/distribution.py +361 -0
  303. truthound/ml/drift_detection/feature.py +442 -0
  304. truthound/ml/drift_detection/multivariate.py +495 -0
  305. truthound/ml/monitoring/__init__.py +88 -0
  306. truthound/ml/monitoring/alerting/__init__.py +33 -0
  307. truthound/ml/monitoring/alerting/handlers.py +427 -0
  308. truthound/ml/monitoring/alerting/rules.py +508 -0
  309. truthound/ml/monitoring/collectors/__init__.py +19 -0
  310. truthound/ml/monitoring/collectors/composite.py +105 -0
  311. truthound/ml/monitoring/collectors/drift.py +324 -0
  312. truthound/ml/monitoring/collectors/performance.py +179 -0
  313. truthound/ml/monitoring/collectors/quality.py +369 -0
  314. truthound/ml/monitoring/monitor.py +536 -0
  315. truthound/ml/monitoring/protocols.py +451 -0
  316. truthound/ml/monitoring/stores/__init__.py +15 -0
  317. truthound/ml/monitoring/stores/memory.py +201 -0
  318. truthound/ml/monitoring/stores/prometheus.py +296 -0
  319. truthound/ml/rule_learning/__init__.py +25 -0
  320. truthound/ml/rule_learning/constraint_miner.py +443 -0
  321. truthound/ml/rule_learning/pattern_learner.py +499 -0
  322. truthound/ml/rule_learning/profile_learner.py +462 -0
  323. truthound/multitenancy/__init__.py +326 -0
  324. truthound/multitenancy/core.py +852 -0
  325. truthound/multitenancy/integration.py +597 -0
  326. truthound/multitenancy/isolation.py +630 -0
  327. truthound/multitenancy/manager.py +770 -0
  328. truthound/multitenancy/middleware.py +765 -0
  329. truthound/multitenancy/quota.py +537 -0
  330. truthound/multitenancy/resolvers.py +603 -0
  331. truthound/multitenancy/storage.py +703 -0
  332. truthound/observability/__init__.py +307 -0
  333. truthound/observability/context.py +531 -0
  334. truthound/observability/instrumentation.py +611 -0
  335. truthound/observability/logging.py +887 -0
  336. truthound/observability/metrics.py +1157 -0
  337. truthound/observability/tracing/__init__.py +178 -0
  338. truthound/observability/tracing/baggage.py +310 -0
  339. truthound/observability/tracing/config.py +426 -0
  340. truthound/observability/tracing/exporter.py +787 -0
  341. truthound/observability/tracing/integration.py +1018 -0
  342. truthound/observability/tracing/otel/__init__.py +146 -0
  343. truthound/observability/tracing/otel/adapter.py +982 -0
  344. truthound/observability/tracing/otel/bridge.py +1177 -0
  345. truthound/observability/tracing/otel/compat.py +681 -0
  346. truthound/observability/tracing/otel/config.py +691 -0
  347. truthound/observability/tracing/otel/detection.py +327 -0
  348. truthound/observability/tracing/otel/protocols.py +426 -0
  349. truthound/observability/tracing/processor.py +561 -0
  350. truthound/observability/tracing/propagator.py +757 -0
  351. truthound/observability/tracing/provider.py +569 -0
  352. truthound/observability/tracing/resource.py +515 -0
  353. truthound/observability/tracing/sampler.py +487 -0
  354. truthound/observability/tracing/span.py +676 -0
  355. truthound/plugins/__init__.py +198 -0
  356. truthound/plugins/base.py +599 -0
  357. truthound/plugins/cli.py +680 -0
  358. truthound/plugins/dependencies/__init__.py +42 -0
  359. truthound/plugins/dependencies/graph.py +422 -0
  360. truthound/plugins/dependencies/resolver.py +417 -0
  361. truthound/plugins/discovery.py +379 -0
  362. truthound/plugins/docs/__init__.py +46 -0
  363. truthound/plugins/docs/extractor.py +444 -0
  364. truthound/plugins/docs/renderer.py +499 -0
  365. truthound/plugins/enterprise_manager.py +877 -0
  366. truthound/plugins/examples/__init__.py +19 -0
  367. truthound/plugins/examples/custom_validators.py +317 -0
  368. truthound/plugins/examples/slack_notifier.py +312 -0
  369. truthound/plugins/examples/xml_reporter.py +254 -0
  370. truthound/plugins/hooks.py +558 -0
  371. truthound/plugins/lifecycle/__init__.py +43 -0
  372. truthound/plugins/lifecycle/hot_reload.py +402 -0
  373. truthound/plugins/lifecycle/manager.py +371 -0
  374. truthound/plugins/manager.py +736 -0
  375. truthound/plugins/registry.py +338 -0
  376. truthound/plugins/security/__init__.py +93 -0
  377. truthound/plugins/security/exceptions.py +332 -0
  378. truthound/plugins/security/policies.py +348 -0
  379. truthound/plugins/security/protocols.py +643 -0
  380. truthound/plugins/security/sandbox/__init__.py +45 -0
  381. truthound/plugins/security/sandbox/context.py +158 -0
  382. truthound/plugins/security/sandbox/engines/__init__.py +19 -0
  383. truthound/plugins/security/sandbox/engines/container.py +379 -0
  384. truthound/plugins/security/sandbox/engines/noop.py +144 -0
  385. truthound/plugins/security/sandbox/engines/process.py +336 -0
  386. truthound/plugins/security/sandbox/factory.py +211 -0
  387. truthound/plugins/security/signing/__init__.py +57 -0
  388. truthound/plugins/security/signing/service.py +330 -0
  389. truthound/plugins/security/signing/trust_store.py +368 -0
  390. truthound/plugins/security/signing/verifier.py +459 -0
  391. truthound/plugins/versioning/__init__.py +41 -0
  392. truthound/plugins/versioning/constraints.py +297 -0
  393. truthound/plugins/versioning/resolver.py +329 -0
  394. truthound/profiler/__init__.py +1729 -0
  395. truthound/profiler/_lazy.py +452 -0
  396. truthound/profiler/ab_testing/__init__.py +80 -0
  397. truthound/profiler/ab_testing/analysis.py +449 -0
  398. truthound/profiler/ab_testing/base.py +257 -0
  399. truthound/profiler/ab_testing/experiment.py +395 -0
  400. truthound/profiler/ab_testing/tracking.py +368 -0
  401. truthound/profiler/auto_threshold.py +1170 -0
  402. truthound/profiler/base.py +579 -0
  403. truthound/profiler/cache_patterns.py +911 -0
  404. truthound/profiler/caching.py +1303 -0
  405. truthound/profiler/column_profiler.py +712 -0
  406. truthound/profiler/comparison.py +1007 -0
  407. truthound/profiler/custom_patterns.py +1170 -0
  408. truthound/profiler/dashboard/__init__.py +50 -0
  409. truthound/profiler/dashboard/app.py +476 -0
  410. truthound/profiler/dashboard/components.py +457 -0
  411. truthound/profiler/dashboard/config.py +72 -0
  412. truthound/profiler/distributed/__init__.py +83 -0
  413. truthound/profiler/distributed/base.py +281 -0
  414. truthound/profiler/distributed/dask_backend.py +498 -0
  415. truthound/profiler/distributed/local_backend.py +293 -0
  416. truthound/profiler/distributed/profiler.py +304 -0
  417. truthound/profiler/distributed/ray_backend.py +374 -0
  418. truthound/profiler/distributed/spark_backend.py +375 -0
  419. truthound/profiler/distributed.py +1366 -0
  420. truthound/profiler/enterprise_sampling.py +1065 -0
  421. truthound/profiler/errors.py +488 -0
  422. truthound/profiler/evolution/__init__.py +91 -0
  423. truthound/profiler/evolution/alerts.py +426 -0
  424. truthound/profiler/evolution/changes.py +206 -0
  425. truthound/profiler/evolution/compatibility.py +365 -0
  426. truthound/profiler/evolution/detector.py +372 -0
  427. truthound/profiler/evolution/protocols.py +121 -0
  428. truthound/profiler/generators/__init__.py +48 -0
  429. truthound/profiler/generators/base.py +384 -0
  430. truthound/profiler/generators/ml_rules.py +375 -0
  431. truthound/profiler/generators/pattern_rules.py +384 -0
  432. truthound/profiler/generators/schema_rules.py +267 -0
  433. truthound/profiler/generators/stats_rules.py +324 -0
  434. truthound/profiler/generators/suite_generator.py +857 -0
  435. truthound/profiler/i18n.py +1542 -0
  436. truthound/profiler/incremental.py +554 -0
  437. truthound/profiler/incremental_validation.py +1710 -0
  438. truthound/profiler/integration/__init__.py +73 -0
  439. truthound/profiler/integration/adapters.py +345 -0
  440. truthound/profiler/integration/context.py +371 -0
  441. truthound/profiler/integration/executor.py +527 -0
  442. truthound/profiler/integration/naming.py +75 -0
  443. truthound/profiler/integration/protocols.py +243 -0
  444. truthound/profiler/memory.py +1185 -0
  445. truthound/profiler/migration/__init__.py +60 -0
  446. truthound/profiler/migration/base.py +345 -0
  447. truthound/profiler/migration/manager.py +444 -0
  448. truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
  449. truthound/profiler/ml/__init__.py +73 -0
  450. truthound/profiler/ml/base.py +244 -0
  451. truthound/profiler/ml/classifier.py +507 -0
  452. truthound/profiler/ml/feature_extraction.py +604 -0
  453. truthound/profiler/ml/pretrained.py +448 -0
  454. truthound/profiler/ml_inference.py +1276 -0
  455. truthound/profiler/native_patterns.py +815 -0
  456. truthound/profiler/observability.py +1184 -0
  457. truthound/profiler/process_timeout.py +1566 -0
  458. truthound/profiler/progress.py +568 -0
  459. truthound/profiler/progress_callbacks.py +1734 -0
  460. truthound/profiler/quality.py +1345 -0
  461. truthound/profiler/resilience.py +1180 -0
  462. truthound/profiler/sampled_matcher.py +794 -0
  463. truthound/profiler/sampling.py +1288 -0
  464. truthound/profiler/scheduling/__init__.py +82 -0
  465. truthound/profiler/scheduling/protocols.py +214 -0
  466. truthound/profiler/scheduling/scheduler.py +474 -0
  467. truthound/profiler/scheduling/storage.py +457 -0
  468. truthound/profiler/scheduling/triggers.py +449 -0
  469. truthound/profiler/schema.py +603 -0
  470. truthound/profiler/streaming.py +685 -0
  471. truthound/profiler/streaming_patterns.py +1354 -0
  472. truthound/profiler/suite_cli.py +625 -0
  473. truthound/profiler/suite_config.py +789 -0
  474. truthound/profiler/suite_export.py +1268 -0
  475. truthound/profiler/table_profiler.py +547 -0
  476. truthound/profiler/timeout.py +565 -0
  477. truthound/profiler/validation.py +1532 -0
  478. truthound/profiler/visualization/__init__.py +118 -0
  479. truthound/profiler/visualization/base.py +346 -0
  480. truthound/profiler/visualization/generator.py +1259 -0
  481. truthound/profiler/visualization/plotly_renderer.py +811 -0
  482. truthound/profiler/visualization/renderers.py +669 -0
  483. truthound/profiler/visualization/sections.py +540 -0
  484. truthound/profiler/visualization.py +2122 -0
  485. truthound/profiler/yaml_validation.py +1151 -0
  486. truthound/py.typed +0 -0
  487. truthound/ratelimit/__init__.py +248 -0
  488. truthound/ratelimit/algorithms.py +1108 -0
  489. truthound/ratelimit/core.py +573 -0
  490. truthound/ratelimit/integration.py +532 -0
  491. truthound/ratelimit/limiter.py +663 -0
  492. truthound/ratelimit/middleware.py +700 -0
  493. truthound/ratelimit/policy.py +792 -0
  494. truthound/ratelimit/storage.py +763 -0
  495. truthound/rbac/__init__.py +340 -0
  496. truthound/rbac/core.py +976 -0
  497. truthound/rbac/integration.py +760 -0
  498. truthound/rbac/manager.py +1052 -0
  499. truthound/rbac/middleware.py +842 -0
  500. truthound/rbac/policy.py +954 -0
  501. truthound/rbac/storage.py +878 -0
  502. truthound/realtime/__init__.py +141 -0
  503. truthound/realtime/adapters/__init__.py +43 -0
  504. truthound/realtime/adapters/base.py +533 -0
  505. truthound/realtime/adapters/kafka.py +487 -0
  506. truthound/realtime/adapters/kinesis.py +479 -0
  507. truthound/realtime/adapters/mock.py +243 -0
  508. truthound/realtime/base.py +553 -0
  509. truthound/realtime/factory.py +382 -0
  510. truthound/realtime/incremental.py +660 -0
  511. truthound/realtime/processing/__init__.py +67 -0
  512. truthound/realtime/processing/exactly_once.py +575 -0
  513. truthound/realtime/processing/state.py +547 -0
  514. truthound/realtime/processing/windows.py +647 -0
  515. truthound/realtime/protocols.py +569 -0
  516. truthound/realtime/streaming.py +605 -0
  517. truthound/realtime/testing/__init__.py +32 -0
  518. truthound/realtime/testing/containers.py +615 -0
  519. truthound/realtime/testing/fixtures.py +484 -0
  520. truthound/report.py +280 -0
  521. truthound/reporters/__init__.py +46 -0
  522. truthound/reporters/_protocols.py +30 -0
  523. truthound/reporters/base.py +324 -0
  524. truthound/reporters/ci/__init__.py +66 -0
  525. truthound/reporters/ci/azure.py +436 -0
  526. truthound/reporters/ci/base.py +509 -0
  527. truthound/reporters/ci/bitbucket.py +567 -0
  528. truthound/reporters/ci/circleci.py +547 -0
  529. truthound/reporters/ci/detection.py +364 -0
  530. truthound/reporters/ci/factory.py +182 -0
  531. truthound/reporters/ci/github.py +388 -0
  532. truthound/reporters/ci/gitlab.py +471 -0
  533. truthound/reporters/ci/jenkins.py +525 -0
  534. truthound/reporters/console_reporter.py +299 -0
  535. truthound/reporters/factory.py +211 -0
  536. truthound/reporters/html_reporter.py +524 -0
  537. truthound/reporters/json_reporter.py +256 -0
  538. truthound/reporters/markdown_reporter.py +280 -0
  539. truthound/reporters/sdk/__init__.py +174 -0
  540. truthound/reporters/sdk/builder.py +558 -0
  541. truthound/reporters/sdk/mixins.py +1150 -0
  542. truthound/reporters/sdk/schema.py +1493 -0
  543. truthound/reporters/sdk/templates.py +666 -0
  544. truthound/reporters/sdk/testing.py +968 -0
  545. truthound/scanners.py +170 -0
  546. truthound/scheduling/__init__.py +122 -0
  547. truthound/scheduling/cron.py +1136 -0
  548. truthound/scheduling/presets.py +212 -0
  549. truthound/schema.py +275 -0
  550. truthound/secrets/__init__.py +173 -0
  551. truthound/secrets/base.py +618 -0
  552. truthound/secrets/cloud.py +682 -0
  553. truthound/secrets/integration.py +507 -0
  554. truthound/secrets/manager.py +633 -0
  555. truthound/secrets/oidc/__init__.py +172 -0
  556. truthound/secrets/oidc/base.py +902 -0
  557. truthound/secrets/oidc/credential_provider.py +623 -0
  558. truthound/secrets/oidc/exchangers.py +1001 -0
  559. truthound/secrets/oidc/github/__init__.py +110 -0
  560. truthound/secrets/oidc/github/claims.py +718 -0
  561. truthound/secrets/oidc/github/enhanced_provider.py +693 -0
  562. truthound/secrets/oidc/github/trust_policy.py +742 -0
  563. truthound/secrets/oidc/github/verification.py +723 -0
  564. truthound/secrets/oidc/github/workflow.py +691 -0
  565. truthound/secrets/oidc/providers.py +825 -0
  566. truthound/secrets/providers.py +506 -0
  567. truthound/secrets/resolver.py +495 -0
  568. truthound/stores/__init__.py +177 -0
  569. truthound/stores/backends/__init__.py +18 -0
  570. truthound/stores/backends/_protocols.py +340 -0
  571. truthound/stores/backends/azure_blob.py +530 -0
  572. truthound/stores/backends/concurrent_filesystem.py +915 -0
  573. truthound/stores/backends/connection_pool.py +1365 -0
  574. truthound/stores/backends/database.py +743 -0
  575. truthound/stores/backends/filesystem.py +538 -0
  576. truthound/stores/backends/gcs.py +399 -0
  577. truthound/stores/backends/memory.py +354 -0
  578. truthound/stores/backends/s3.py +434 -0
  579. truthound/stores/backpressure/__init__.py +84 -0
  580. truthound/stores/backpressure/base.py +375 -0
  581. truthound/stores/backpressure/circuit_breaker.py +434 -0
  582. truthound/stores/backpressure/monitor.py +376 -0
  583. truthound/stores/backpressure/strategies.py +677 -0
  584. truthound/stores/base.py +551 -0
  585. truthound/stores/batching/__init__.py +65 -0
  586. truthound/stores/batching/base.py +305 -0
  587. truthound/stores/batching/buffer.py +370 -0
  588. truthound/stores/batching/store.py +248 -0
  589. truthound/stores/batching/writer.py +521 -0
  590. truthound/stores/caching/__init__.py +60 -0
  591. truthound/stores/caching/backends.py +684 -0
  592. truthound/stores/caching/base.py +356 -0
  593. truthound/stores/caching/store.py +305 -0
  594. truthound/stores/compression/__init__.py +193 -0
  595. truthound/stores/compression/adaptive.py +694 -0
  596. truthound/stores/compression/base.py +514 -0
  597. truthound/stores/compression/pipeline.py +868 -0
  598. truthound/stores/compression/providers.py +672 -0
  599. truthound/stores/compression/streaming.py +832 -0
  600. truthound/stores/concurrency/__init__.py +81 -0
  601. truthound/stores/concurrency/atomic.py +556 -0
  602. truthound/stores/concurrency/index.py +775 -0
  603. truthound/stores/concurrency/locks.py +576 -0
  604. truthound/stores/concurrency/manager.py +482 -0
  605. truthound/stores/encryption/__init__.py +297 -0
  606. truthound/stores/encryption/base.py +952 -0
  607. truthound/stores/encryption/keys.py +1191 -0
  608. truthound/stores/encryption/pipeline.py +903 -0
  609. truthound/stores/encryption/providers.py +953 -0
  610. truthound/stores/encryption/streaming.py +950 -0
  611. truthound/stores/expectations.py +227 -0
  612. truthound/stores/factory.py +246 -0
  613. truthound/stores/migration/__init__.py +75 -0
  614. truthound/stores/migration/base.py +480 -0
  615. truthound/stores/migration/manager.py +347 -0
  616. truthound/stores/migration/registry.py +382 -0
  617. truthound/stores/migration/store.py +559 -0
  618. truthound/stores/observability/__init__.py +106 -0
  619. truthound/stores/observability/audit.py +718 -0
  620. truthound/stores/observability/config.py +270 -0
  621. truthound/stores/observability/factory.py +208 -0
  622. truthound/stores/observability/metrics.py +636 -0
  623. truthound/stores/observability/protocols.py +410 -0
  624. truthound/stores/observability/store.py +570 -0
  625. truthound/stores/observability/tracing.py +784 -0
  626. truthound/stores/replication/__init__.py +76 -0
  627. truthound/stores/replication/base.py +260 -0
  628. truthound/stores/replication/monitor.py +269 -0
  629. truthound/stores/replication/store.py +439 -0
  630. truthound/stores/replication/syncer.py +391 -0
  631. truthound/stores/results.py +359 -0
  632. truthound/stores/retention/__init__.py +77 -0
  633. truthound/stores/retention/base.py +378 -0
  634. truthound/stores/retention/policies.py +621 -0
  635. truthound/stores/retention/scheduler.py +279 -0
  636. truthound/stores/retention/store.py +526 -0
  637. truthound/stores/streaming/__init__.py +138 -0
  638. truthound/stores/streaming/base.py +801 -0
  639. truthound/stores/streaming/database.py +984 -0
  640. truthound/stores/streaming/filesystem.py +719 -0
  641. truthound/stores/streaming/reader.py +629 -0
  642. truthound/stores/streaming/s3.py +843 -0
  643. truthound/stores/streaming/writer.py +790 -0
  644. truthound/stores/tiering/__init__.py +108 -0
  645. truthound/stores/tiering/base.py +462 -0
  646. truthound/stores/tiering/manager.py +249 -0
  647. truthound/stores/tiering/policies.py +692 -0
  648. truthound/stores/tiering/store.py +526 -0
  649. truthound/stores/versioning/__init__.py +56 -0
  650. truthound/stores/versioning/base.py +376 -0
  651. truthound/stores/versioning/store.py +660 -0
  652. truthound/stores/versioning/strategies.py +353 -0
  653. truthound/types.py +56 -0
  654. truthound/validators/__init__.py +774 -0
  655. truthound/validators/aggregate/__init__.py +27 -0
  656. truthound/validators/aggregate/central.py +116 -0
  657. truthound/validators/aggregate/extremes.py +116 -0
  658. truthound/validators/aggregate/spread.py +118 -0
  659. truthound/validators/aggregate/sum.py +64 -0
  660. truthound/validators/aggregate/type.py +78 -0
  661. truthound/validators/anomaly/__init__.py +93 -0
  662. truthound/validators/anomaly/base.py +431 -0
  663. truthound/validators/anomaly/ml_based.py +1190 -0
  664. truthound/validators/anomaly/multivariate.py +647 -0
  665. truthound/validators/anomaly/statistical.py +599 -0
  666. truthound/validators/base.py +1089 -0
  667. truthound/validators/business_rule/__init__.py +46 -0
  668. truthound/validators/business_rule/base.py +147 -0
  669. truthound/validators/business_rule/checksum.py +509 -0
  670. truthound/validators/business_rule/financial.py +526 -0
  671. truthound/validators/cache.py +733 -0
  672. truthound/validators/completeness/__init__.py +39 -0
  673. truthound/validators/completeness/conditional.py +73 -0
  674. truthound/validators/completeness/default.py +98 -0
  675. truthound/validators/completeness/empty.py +103 -0
  676. truthound/validators/completeness/nan.py +337 -0
  677. truthound/validators/completeness/null.py +152 -0
  678. truthound/validators/cross_table/__init__.py +17 -0
  679. truthound/validators/cross_table/aggregate.py +333 -0
  680. truthound/validators/cross_table/row_count.py +122 -0
  681. truthound/validators/datetime/__init__.py +29 -0
  682. truthound/validators/datetime/format.py +78 -0
  683. truthound/validators/datetime/freshness.py +269 -0
  684. truthound/validators/datetime/order.py +73 -0
  685. truthound/validators/datetime/parseable.py +185 -0
  686. truthound/validators/datetime/range.py +202 -0
  687. truthound/validators/datetime/timezone.py +69 -0
  688. truthound/validators/distribution/__init__.py +49 -0
  689. truthound/validators/distribution/distribution.py +128 -0
  690. truthound/validators/distribution/monotonic.py +119 -0
  691. truthound/validators/distribution/outlier.py +178 -0
  692. truthound/validators/distribution/quantile.py +80 -0
  693. truthound/validators/distribution/range.py +254 -0
  694. truthound/validators/distribution/set.py +125 -0
  695. truthound/validators/distribution/statistical.py +459 -0
  696. truthound/validators/drift/__init__.py +79 -0
  697. truthound/validators/drift/base.py +427 -0
  698. truthound/validators/drift/multi_feature.py +401 -0
  699. truthound/validators/drift/numeric.py +395 -0
  700. truthound/validators/drift/psi.py +446 -0
  701. truthound/validators/drift/statistical.py +510 -0
  702. truthound/validators/enterprise.py +1658 -0
  703. truthound/validators/geospatial/__init__.py +80 -0
  704. truthound/validators/geospatial/base.py +97 -0
  705. truthound/validators/geospatial/boundary.py +238 -0
  706. truthound/validators/geospatial/coordinate.py +351 -0
  707. truthound/validators/geospatial/distance.py +399 -0
  708. truthound/validators/geospatial/polygon.py +665 -0
  709. truthound/validators/i18n/__init__.py +308 -0
  710. truthound/validators/i18n/bidi.py +571 -0
  711. truthound/validators/i18n/catalogs.py +570 -0
  712. truthound/validators/i18n/dialects.py +763 -0
  713. truthound/validators/i18n/extended_catalogs.py +549 -0
  714. truthound/validators/i18n/formatting.py +1434 -0
  715. truthound/validators/i18n/loader.py +1020 -0
  716. truthound/validators/i18n/messages.py +521 -0
  717. truthound/validators/i18n/plural.py +683 -0
  718. truthound/validators/i18n/protocols.py +855 -0
  719. truthound/validators/i18n/tms.py +1162 -0
  720. truthound/validators/localization/__init__.py +53 -0
  721. truthound/validators/localization/base.py +122 -0
  722. truthound/validators/localization/chinese.py +362 -0
  723. truthound/validators/localization/japanese.py +275 -0
  724. truthound/validators/localization/korean.py +524 -0
  725. truthound/validators/memory/__init__.py +94 -0
  726. truthound/validators/memory/approximate_knn.py +506 -0
  727. truthound/validators/memory/base.py +547 -0
  728. truthound/validators/memory/sgd_online.py +719 -0
  729. truthound/validators/memory/streaming_ecdf.py +753 -0
  730. truthound/validators/ml_feature/__init__.py +54 -0
  731. truthound/validators/ml_feature/base.py +249 -0
  732. truthound/validators/ml_feature/correlation.py +299 -0
  733. truthound/validators/ml_feature/leakage.py +344 -0
  734. truthound/validators/ml_feature/null_impact.py +270 -0
  735. truthound/validators/ml_feature/scale.py +264 -0
  736. truthound/validators/multi_column/__init__.py +89 -0
  737. truthound/validators/multi_column/arithmetic.py +284 -0
  738. truthound/validators/multi_column/base.py +231 -0
  739. truthound/validators/multi_column/comparison.py +273 -0
  740. truthound/validators/multi_column/consistency.py +312 -0
  741. truthound/validators/multi_column/statistical.py +299 -0
  742. truthound/validators/optimization/__init__.py +164 -0
  743. truthound/validators/optimization/aggregation.py +563 -0
  744. truthound/validators/optimization/covariance.py +556 -0
  745. truthound/validators/optimization/geo.py +626 -0
  746. truthound/validators/optimization/graph.py +587 -0
  747. truthound/validators/optimization/orchestrator.py +970 -0
  748. truthound/validators/optimization/profiling.py +1312 -0
  749. truthound/validators/privacy/__init__.py +223 -0
  750. truthound/validators/privacy/base.py +635 -0
  751. truthound/validators/privacy/ccpa.py +670 -0
  752. truthound/validators/privacy/gdpr.py +728 -0
  753. truthound/validators/privacy/global_patterns.py +604 -0
  754. truthound/validators/privacy/plugins.py +867 -0
  755. truthound/validators/profiling/__init__.py +52 -0
  756. truthound/validators/profiling/base.py +175 -0
  757. truthound/validators/profiling/cardinality.py +312 -0
  758. truthound/validators/profiling/entropy.py +391 -0
  759. truthound/validators/profiling/frequency.py +455 -0
  760. truthound/validators/pushdown_support.py +660 -0
  761. truthound/validators/query/__init__.py +91 -0
  762. truthound/validators/query/aggregate.py +346 -0
  763. truthound/validators/query/base.py +246 -0
  764. truthound/validators/query/column.py +249 -0
  765. truthound/validators/query/expression.py +274 -0
  766. truthound/validators/query/result.py +323 -0
  767. truthound/validators/query/row_count.py +264 -0
  768. truthound/validators/referential/__init__.py +80 -0
  769. truthound/validators/referential/base.py +395 -0
  770. truthound/validators/referential/cascade.py +391 -0
  771. truthound/validators/referential/circular.py +563 -0
  772. truthound/validators/referential/foreign_key.py +624 -0
  773. truthound/validators/referential/orphan.py +485 -0
  774. truthound/validators/registry.py +112 -0
  775. truthound/validators/schema/__init__.py +41 -0
  776. truthound/validators/schema/column_count.py +142 -0
  777. truthound/validators/schema/column_exists.py +80 -0
  778. truthound/validators/schema/column_order.py +82 -0
  779. truthound/validators/schema/column_pair.py +85 -0
  780. truthound/validators/schema/column_pair_set.py +195 -0
  781. truthound/validators/schema/column_type.py +94 -0
  782. truthound/validators/schema/multi_column.py +53 -0
  783. truthound/validators/schema/multi_column_aggregate.py +175 -0
  784. truthound/validators/schema/referential.py +274 -0
  785. truthound/validators/schema/table_schema.py +91 -0
  786. truthound/validators/schema_validator.py +219 -0
  787. truthound/validators/sdk/__init__.py +250 -0
  788. truthound/validators/sdk/builder.py +680 -0
  789. truthound/validators/sdk/decorators.py +474 -0
  790. truthound/validators/sdk/enterprise/__init__.py +211 -0
  791. truthound/validators/sdk/enterprise/docs.py +725 -0
  792. truthound/validators/sdk/enterprise/fuzzing.py +659 -0
  793. truthound/validators/sdk/enterprise/licensing.py +709 -0
  794. truthound/validators/sdk/enterprise/manager.py +543 -0
  795. truthound/validators/sdk/enterprise/resources.py +628 -0
  796. truthound/validators/sdk/enterprise/sandbox.py +766 -0
  797. truthound/validators/sdk/enterprise/signing.py +603 -0
  798. truthound/validators/sdk/enterprise/templates.py +865 -0
  799. truthound/validators/sdk/enterprise/versioning.py +659 -0
  800. truthound/validators/sdk/templates.py +757 -0
  801. truthound/validators/sdk/testing.py +807 -0
  802. truthound/validators/security/__init__.py +181 -0
  803. truthound/validators/security/redos/__init__.py +182 -0
  804. truthound/validators/security/redos/core.py +861 -0
  805. truthound/validators/security/redos/cpu_monitor.py +593 -0
  806. truthound/validators/security/redos/cve_database.py +791 -0
  807. truthound/validators/security/redos/ml/__init__.py +155 -0
  808. truthound/validators/security/redos/ml/base.py +785 -0
  809. truthound/validators/security/redos/ml/datasets.py +618 -0
  810. truthound/validators/security/redos/ml/features.py +359 -0
  811. truthound/validators/security/redos/ml/models.py +1000 -0
  812. truthound/validators/security/redos/ml/predictor.py +507 -0
  813. truthound/validators/security/redos/ml/storage.py +632 -0
  814. truthound/validators/security/redos/ml/training.py +571 -0
  815. truthound/validators/security/redos/ml_analyzer.py +937 -0
  816. truthound/validators/security/redos/optimizer.py +674 -0
  817. truthound/validators/security/redos/profiler.py +682 -0
  818. truthound/validators/security/redos/re2_engine.py +709 -0
  819. truthound/validators/security/redos.py +886 -0
  820. truthound/validators/security/sql_security.py +1247 -0
  821. truthound/validators/streaming/__init__.py +126 -0
  822. truthound/validators/streaming/base.py +292 -0
  823. truthound/validators/streaming/completeness.py +210 -0
  824. truthound/validators/streaming/mixin.py +575 -0
  825. truthound/validators/streaming/range.py +308 -0
  826. truthound/validators/streaming/sources.py +846 -0
  827. truthound/validators/string/__init__.py +57 -0
  828. truthound/validators/string/casing.py +158 -0
  829. truthound/validators/string/charset.py +96 -0
  830. truthound/validators/string/format.py +501 -0
  831. truthound/validators/string/json.py +77 -0
  832. truthound/validators/string/json_schema.py +184 -0
  833. truthound/validators/string/length.py +104 -0
  834. truthound/validators/string/like_pattern.py +237 -0
  835. truthound/validators/string/regex.py +202 -0
  836. truthound/validators/string/regex_extended.py +435 -0
  837. truthound/validators/table/__init__.py +88 -0
  838. truthound/validators/table/base.py +78 -0
  839. truthound/validators/table/column_count.py +198 -0
  840. truthound/validators/table/freshness.py +362 -0
  841. truthound/validators/table/row_count.py +251 -0
  842. truthound/validators/table/schema.py +333 -0
  843. truthound/validators/table/size.py +285 -0
  844. truthound/validators/timeout/__init__.py +102 -0
  845. truthound/validators/timeout/advanced/__init__.py +247 -0
  846. truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
  847. truthound/validators/timeout/advanced/prediction.py +773 -0
  848. truthound/validators/timeout/advanced/priority.py +618 -0
  849. truthound/validators/timeout/advanced/redis_backend.py +770 -0
  850. truthound/validators/timeout/advanced/retry.py +721 -0
  851. truthound/validators/timeout/advanced/sampling.py +788 -0
  852. truthound/validators/timeout/advanced/sla.py +661 -0
  853. truthound/validators/timeout/advanced/telemetry.py +804 -0
  854. truthound/validators/timeout/cascade.py +477 -0
  855. truthound/validators/timeout/deadline.py +657 -0
  856. truthound/validators/timeout/degradation.py +525 -0
  857. truthound/validators/timeout/distributed.py +597 -0
  858. truthound/validators/timeseries/__init__.py +89 -0
  859. truthound/validators/timeseries/base.py +326 -0
  860. truthound/validators/timeseries/completeness.py +617 -0
  861. truthound/validators/timeseries/gap.py +485 -0
  862. truthound/validators/timeseries/monotonic.py +310 -0
  863. truthound/validators/timeseries/seasonality.py +422 -0
  864. truthound/validators/timeseries/trend.py +510 -0
  865. truthound/validators/uniqueness/__init__.py +59 -0
  866. truthound/validators/uniqueness/approximate.py +475 -0
  867. truthound/validators/uniqueness/distinct_values.py +253 -0
  868. truthound/validators/uniqueness/duplicate.py +118 -0
  869. truthound/validators/uniqueness/primary_key.py +140 -0
  870. truthound/validators/uniqueness/unique.py +191 -0
  871. truthound/validators/uniqueness/within_record.py +599 -0
  872. truthound/validators/utils.py +756 -0
  873. truthound-1.0.8.dist-info/METADATA +474 -0
  874. truthound-1.0.8.dist-info/RECORD +877 -0
  875. truthound-1.0.8.dist-info/WHEEL +4 -0
  876. truthound-1.0.8.dist-info/entry_points.txt +2 -0
  877. 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