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,1434 @@
1
+ """Locale-Aware Number and Date Formatting.
2
+
3
+ This module provides comprehensive locale-aware formatting for:
4
+ - Numbers (decimal, currency, percent, scientific, compact, ordinal)
5
+ - Dates (short, medium, long, full, ISO, relative)
6
+ - Times (short, medium, long, full)
7
+ - Durations (human-readable time spans)
8
+
9
+ Features:
10
+ - CLDR-based formatting patterns
11
+ - Locale-specific number symbols (decimal, grouping, etc.)
12
+ - Multiple calendar support
13
+ - Relative time formatting ("2 days ago")
14
+ - Currency formatting with symbol positioning
15
+
16
+ Usage:
17
+ from truthound.validators.i18n.formatting import (
18
+ LocaleNumberFormatter,
19
+ LocaleDateFormatter,
20
+ format_number,
21
+ format_date,
22
+ format_currency,
23
+ )
24
+
25
+ # Format numbers
26
+ format_number(1234567.89, "de") # "1.234.567,89"
27
+ format_number(1234567.89, "en") # "1,234,567.89"
28
+
29
+ # Format currency
30
+ format_currency(1234.56, "USD", "en") # "$1,234.56"
31
+ format_currency(1234.56, "EUR", "de") # "1.234,56 €"
32
+
33
+ # Format dates
34
+ format_date(datetime.now(), "ko", DateStyle.LONG) # "2024년 12월 28일"
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import math
40
+ import re
41
+ from dataclasses import dataclass, field
42
+ from datetime import datetime, date, time, timedelta
43
+ from decimal import Decimal, ROUND_HALF_UP
44
+ from typing import Any
45
+
46
+ from truthound.validators.i18n.protocols import (
47
+ BaseDateFormatter,
48
+ BaseNumberFormatter,
49
+ DateStyle,
50
+ FormattedDate,
51
+ FormattedNumber,
52
+ LocaleInfo,
53
+ NumberStyle,
54
+ TextDirection,
55
+ TimeStyle,
56
+ )
57
+
58
+
59
+ # ==============================================================================
60
+ # Locale Data: Number Symbols
61
+ # ==============================================================================
62
+
63
+ @dataclass
64
+ class NumberSymbols:
65
+ """Locale-specific number symbols.
66
+
67
+ Based on CLDR number symbols data.
68
+ """
69
+ decimal: str = "."
70
+ group: str = ","
71
+ minus: str = "-"
72
+ plus: str = "+"
73
+ percent: str = "%"
74
+ per_mille: str = "‰"
75
+ exponential: str = "E"
76
+ infinity: str = "∞"
77
+ nan: str = "NaN"
78
+ currency_decimal: str | None = None
79
+ currency_group: str | None = None
80
+
81
+ def get_currency_decimal(self) -> str:
82
+ """Get decimal separator for currency (falls back to decimal)."""
83
+ return self.currency_decimal or self.decimal
84
+
85
+ def get_currency_group(self) -> str:
86
+ """Get grouping separator for currency (falls back to group)."""
87
+ return self.currency_group or self.group
88
+
89
+
90
+ # Number symbols by locale
91
+ _NUMBER_SYMBOLS: dict[str, NumberSymbols] = {
92
+ # Default (English)
93
+ "en": NumberSymbols(),
94
+
95
+ # German, Austrian German
96
+ "de": NumberSymbols(decimal=",", group="."),
97
+ "de_AT": NumberSymbols(decimal=",", group=" "),
98
+
99
+ # French
100
+ "fr": NumberSymbols(decimal=",", group=" ", currency_group=" "),
101
+ "fr_CH": NumberSymbols(decimal=",", group=" "),
102
+
103
+ # Spanish
104
+ "es": NumberSymbols(decimal=",", group="."),
105
+ "es_MX": NumberSymbols(decimal=".", group=","),
106
+
107
+ # Italian
108
+ "it": NumberSymbols(decimal=",", group="."),
109
+
110
+ # Portuguese
111
+ "pt": NumberSymbols(decimal=",", group="."),
112
+ "pt_BR": NumberSymbols(decimal=",", group="."),
113
+
114
+ # Russian
115
+ "ru": NumberSymbols(decimal=",", group=" "),
116
+
117
+ # Polish
118
+ "pl": NumberSymbols(decimal=",", group=" "),
119
+
120
+ # Dutch
121
+ "nl": NumberSymbols(decimal=",", group="."),
122
+
123
+ # Czech
124
+ "cs": NumberSymbols(decimal=",", group=" "),
125
+
126
+ # Swedish, Norwegian, Danish, Finnish
127
+ "sv": NumberSymbols(decimal=",", group=" "),
128
+ "no": NumberSymbols(decimal=",", group=" "),
129
+ "nb": NumberSymbols(decimal=",", group=" "),
130
+ "da": NumberSymbols(decimal=",", group="."),
131
+ "fi": NumberSymbols(decimal=",", group=" "),
132
+
133
+ # Korean, Japanese, Chinese
134
+ "ko": NumberSymbols(decimal=".", group=","),
135
+ "ja": NumberSymbols(decimal=".", group=","),
136
+ "zh": NumberSymbols(decimal=".", group=","),
137
+ "zh_TW": NumberSymbols(decimal=".", group=","),
138
+
139
+ # Arabic
140
+ "ar": NumberSymbols(decimal="٫", group="٬", percent="٪", minus="-"),
141
+
142
+ # Hebrew
143
+ "he": NumberSymbols(decimal=".", group=","),
144
+
145
+ # Hindi
146
+ "hi": NumberSymbols(decimal=".", group=","),
147
+
148
+ # Thai
149
+ "th": NumberSymbols(decimal=".", group=","),
150
+
151
+ # Vietnamese
152
+ "vi": NumberSymbols(decimal=",", group="."),
153
+
154
+ # Indonesian
155
+ "id": NumberSymbols(decimal=",", group="."),
156
+
157
+ # Turkish
158
+ "tr": NumberSymbols(decimal=",", group="."),
159
+
160
+ # Greek
161
+ "el": NumberSymbols(decimal=",", group="."),
162
+
163
+ # Hungarian
164
+ "hu": NumberSymbols(decimal=",", group=" "),
165
+
166
+ # Romanian
167
+ "ro": NumberSymbols(decimal=",", group="."),
168
+
169
+ # Ukrainian
170
+ "uk": NumberSymbols(decimal=",", group=" "),
171
+
172
+ # Swiss locales
173
+ "de_CH": NumberSymbols(decimal=".", group="'"),
174
+ "it_CH": NumberSymbols(decimal=".", group="'"),
175
+
176
+ # Indian English
177
+ "en_IN": NumberSymbols(decimal=".", group=","),
178
+ }
179
+
180
+
181
+ def get_number_symbols(locale: LocaleInfo) -> NumberSymbols:
182
+ """Get number symbols for a locale.
183
+
184
+ Args:
185
+ locale: Target locale
186
+
187
+ Returns:
188
+ NumberSymbols for the locale
189
+ """
190
+ # Try full locale tag first
191
+ key = f"{locale.language}_{locale.region}" if locale.region else locale.language
192
+ if key in _NUMBER_SYMBOLS:
193
+ return _NUMBER_SYMBOLS[key]
194
+
195
+ # Fall back to language only
196
+ if locale.language in _NUMBER_SYMBOLS:
197
+ return _NUMBER_SYMBOLS[locale.language]
198
+
199
+ # Default to English
200
+ return _NUMBER_SYMBOLS["en"]
201
+
202
+
203
+ # ==============================================================================
204
+ # Locale Data: Currency Information
205
+ # ==============================================================================
206
+
207
+ @dataclass
208
+ class CurrencyInfo:
209
+ """Currency formatting information."""
210
+ code: str
211
+ symbol: str
212
+ narrow_symbol: str | None = None
213
+ name: str = ""
214
+ decimal_digits: int = 2
215
+ rounding: int = 0
216
+
217
+
218
+ # Common currencies
219
+ _CURRENCIES: dict[str, CurrencyInfo] = {
220
+ "USD": CurrencyInfo("USD", "$", "$", "US Dollar", 2),
221
+ "EUR": CurrencyInfo("EUR", "€", "€", "Euro", 2),
222
+ "GBP": CurrencyInfo("GBP", "£", "£", "British Pound", 2),
223
+ "JPY": CurrencyInfo("JPY", "¥", "¥", "Japanese Yen", 0),
224
+ "CNY": CurrencyInfo("CNY", "¥", "¥", "Chinese Yuan", 2),
225
+ "KRW": CurrencyInfo("KRW", "₩", "₩", "Korean Won", 0),
226
+ "INR": CurrencyInfo("INR", "₹", "₹", "Indian Rupee", 2),
227
+ "BRL": CurrencyInfo("BRL", "R$", "R$", "Brazilian Real", 2),
228
+ "RUB": CurrencyInfo("RUB", "₽", "₽", "Russian Ruble", 2),
229
+ "AUD": CurrencyInfo("AUD", "A$", "$", "Australian Dollar", 2),
230
+ "CAD": CurrencyInfo("CAD", "CA$", "$", "Canadian Dollar", 2),
231
+ "CHF": CurrencyInfo("CHF", "CHF", "CHF", "Swiss Franc", 2),
232
+ "HKD": CurrencyInfo("HKD", "HK$", "$", "Hong Kong Dollar", 2),
233
+ "SGD": CurrencyInfo("SGD", "S$", "$", "Singapore Dollar", 2),
234
+ "SEK": CurrencyInfo("SEK", "kr", "kr", "Swedish Krona", 2),
235
+ "NOK": CurrencyInfo("NOK", "kr", "kr", "Norwegian Krone", 2),
236
+ "DKK": CurrencyInfo("DKK", "kr", "kr", "Danish Krone", 2),
237
+ "NZD": CurrencyInfo("NZD", "NZ$", "$", "New Zealand Dollar", 2),
238
+ "MXN": CurrencyInfo("MXN", "MX$", "$", "Mexican Peso", 2),
239
+ "ZAR": CurrencyInfo("ZAR", "R", "R", "South African Rand", 2),
240
+ "TRY": CurrencyInfo("TRY", "₺", "₺", "Turkish Lira", 2),
241
+ "PLN": CurrencyInfo("PLN", "zł", "zł", "Polish Zloty", 2),
242
+ "THB": CurrencyInfo("THB", "฿", "฿", "Thai Baht", 2),
243
+ "IDR": CurrencyInfo("IDR", "Rp", "Rp", "Indonesian Rupiah", 0),
244
+ "MYR": CurrencyInfo("MYR", "RM", "RM", "Malaysian Ringgit", 2),
245
+ "PHP": CurrencyInfo("PHP", "₱", "₱", "Philippine Peso", 2),
246
+ "VND": CurrencyInfo("VND", "₫", "₫", "Vietnamese Dong", 0),
247
+ "AED": CurrencyInfo("AED", "د.إ", "د.إ", "UAE Dirham", 2),
248
+ "SAR": CurrencyInfo("SAR", "ر.س", "ر.س", "Saudi Riyal", 2),
249
+ "ILS": CurrencyInfo("ILS", "₪", "₪", "Israeli Shekel", 2),
250
+ }
251
+
252
+
253
+ def get_currency_info(code: str) -> CurrencyInfo:
254
+ """Get currency information.
255
+
256
+ Args:
257
+ code: ISO 4217 currency code
258
+
259
+ Returns:
260
+ CurrencyInfo for the currency
261
+ """
262
+ return _CURRENCIES.get(code.upper(), CurrencyInfo(code, code, code, code))
263
+
264
+
265
+ # ==============================================================================
266
+ # Locale Data: Date/Time Patterns
267
+ # ==============================================================================
268
+
269
+ @dataclass
270
+ class DateTimePatterns:
271
+ """Locale-specific date/time patterns."""
272
+ # Date patterns by style
273
+ date_short: str = "M/d/yy"
274
+ date_medium: str = "MMM d, y"
275
+ date_long: str = "MMMM d, y"
276
+ date_full: str = "EEEE, MMMM d, y"
277
+
278
+ # Time patterns by style
279
+ time_short: str = "h:mm a"
280
+ time_medium: str = "h:mm:ss a"
281
+ time_long: str = "h:mm:ss a z"
282
+ time_full: str = "h:mm:ss a zzzz"
283
+
284
+ # Combined datetime pattern
285
+ datetime_pattern: str = "{date} {time}"
286
+
287
+ # Month names
288
+ months_wide: list[str] = field(default_factory=lambda: [
289
+ "January", "February", "March", "April", "May", "June",
290
+ "July", "August", "September", "October", "November", "December"
291
+ ])
292
+ months_abbreviated: list[str] = field(default_factory=lambda: [
293
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
294
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
295
+ ])
296
+
297
+ # Day names
298
+ days_wide: list[str] = field(default_factory=lambda: [
299
+ "Sunday", "Monday", "Tuesday", "Wednesday",
300
+ "Thursday", "Friday", "Saturday"
301
+ ])
302
+ days_abbreviated: list[str] = field(default_factory=lambda: [
303
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
304
+ ])
305
+
306
+ # AM/PM
307
+ am: str = "AM"
308
+ pm: str = "PM"
309
+
310
+ # Era names
311
+ era_abbreviated: list[str] = field(default_factory=lambda: ["BC", "AD"])
312
+ era_wide: list[str] = field(default_factory=lambda: ["Before Christ", "Anno Domini"])
313
+
314
+
315
+ # Date patterns by locale
316
+ _DATE_PATTERNS: dict[str, DateTimePatterns] = {
317
+ "en": DateTimePatterns(),
318
+
319
+ "en_GB": DateTimePatterns(
320
+ date_short="dd/MM/y",
321
+ date_medium="d MMM y",
322
+ date_long="d MMMM y",
323
+ date_full="EEEE, d MMMM y",
324
+ ),
325
+
326
+ "de": DateTimePatterns(
327
+ date_short="dd.MM.yy",
328
+ date_medium="dd.MM.y",
329
+ date_long="d. MMMM y",
330
+ date_full="EEEE, d. MMMM y",
331
+ time_short="HH:mm",
332
+ time_medium="HH:mm:ss",
333
+ months_wide=[
334
+ "Januar", "Februar", "März", "April", "Mai", "Juni",
335
+ "Juli", "August", "September", "Oktober", "November", "Dezember"
336
+ ],
337
+ months_abbreviated=["Jan.", "Feb.", "März", "Apr.", "Mai", "Juni",
338
+ "Juli", "Aug.", "Sep.", "Okt.", "Nov.", "Dez."],
339
+ days_wide=["Sonntag", "Montag", "Dienstag", "Mittwoch",
340
+ "Donnerstag", "Freitag", "Samstag"],
341
+ days_abbreviated=["So.", "Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa."],
342
+ ),
343
+
344
+ "fr": DateTimePatterns(
345
+ date_short="dd/MM/y",
346
+ date_medium="d MMM y",
347
+ date_long="d MMMM y",
348
+ date_full="EEEE d MMMM y",
349
+ time_short="HH:mm",
350
+ time_medium="HH:mm:ss",
351
+ months_wide=[
352
+ "janvier", "février", "mars", "avril", "mai", "juin",
353
+ "juillet", "août", "septembre", "octobre", "novembre", "décembre"
354
+ ],
355
+ months_abbreviated=["janv.", "févr.", "mars", "avr.", "mai", "juin",
356
+ "juil.", "août", "sept.", "oct.", "nov.", "déc."],
357
+ days_wide=["dimanche", "lundi", "mardi", "mercredi",
358
+ "jeudi", "vendredi", "samedi"],
359
+ days_abbreviated=["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
360
+ ),
361
+
362
+ "es": DateTimePatterns(
363
+ date_short="d/M/yy",
364
+ date_medium="d MMM y",
365
+ date_long="d 'de' MMMM 'de' y",
366
+ date_full="EEEE, d 'de' MMMM 'de' y",
367
+ time_short="H:mm",
368
+ time_medium="H:mm:ss",
369
+ months_wide=[
370
+ "enero", "febrero", "marzo", "abril", "mayo", "junio",
371
+ "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"
372
+ ],
373
+ months_abbreviated=["ene.", "feb.", "mar.", "abr.", "may.", "jun.",
374
+ "jul.", "ago.", "sept.", "oct.", "nov.", "dic."],
375
+ days_wide=["domingo", "lunes", "martes", "miércoles",
376
+ "jueves", "viernes", "sábado"],
377
+ days_abbreviated=["dom.", "lun.", "mar.", "mié.", "jue.", "vie.", "sáb."],
378
+ ),
379
+
380
+ "it": DateTimePatterns(
381
+ date_short="dd/MM/yy",
382
+ date_medium="d MMM y",
383
+ date_long="d MMMM y",
384
+ date_full="EEEE d MMMM y",
385
+ time_short="HH:mm",
386
+ time_medium="HH:mm:ss",
387
+ months_wide=[
388
+ "gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno",
389
+ "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"
390
+ ],
391
+ months_abbreviated=["gen", "feb", "mar", "apr", "mag", "giu",
392
+ "lug", "ago", "set", "ott", "nov", "dic"],
393
+ days_wide=["domenica", "lunedì", "martedì", "mercoledì",
394
+ "giovedì", "venerdì", "sabato"],
395
+ days_abbreviated=["dom", "lun", "mar", "mer", "gio", "ven", "sab"],
396
+ ),
397
+
398
+ "pt": DateTimePatterns(
399
+ date_short="dd/MM/y",
400
+ date_medium="d 'de' MMM 'de' y",
401
+ date_long="d 'de' MMMM 'de' y",
402
+ date_full="EEEE, d 'de' MMMM 'de' y",
403
+ time_short="HH:mm",
404
+ time_medium="HH:mm:ss",
405
+ months_wide=[
406
+ "janeiro", "fevereiro", "março", "abril", "maio", "junho",
407
+ "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"
408
+ ],
409
+ months_abbreviated=["jan.", "fev.", "mar.", "abr.", "mai.", "jun.",
410
+ "jul.", "ago.", "set.", "out.", "nov.", "dez."],
411
+ days_wide=["domingo", "segunda-feira", "terça-feira", "quarta-feira",
412
+ "quinta-feira", "sexta-feira", "sábado"],
413
+ days_abbreviated=["dom.", "seg.", "ter.", "qua.", "qui.", "sex.", "sáb."],
414
+ ),
415
+
416
+ "ru": DateTimePatterns(
417
+ date_short="dd.MM.y",
418
+ date_medium="d MMM y 'г.'",
419
+ date_long="d MMMM y 'г.'",
420
+ date_full="EEEE, d MMMM y 'г.'",
421
+ time_short="HH:mm",
422
+ time_medium="HH:mm:ss",
423
+ months_wide=[
424
+ "января", "февраля", "марта", "апреля", "мая", "июня",
425
+ "июля", "августа", "сентября", "октября", "ноября", "декабря"
426
+ ],
427
+ months_abbreviated=["янв.", "февр.", "мар.", "апр.", "мая", "июн.",
428
+ "июл.", "авг.", "сент.", "окт.", "нояб.", "дек."],
429
+ days_wide=["воскресенье", "понедельник", "вторник", "среда",
430
+ "четверг", "пятница", "суббота"],
431
+ days_abbreviated=["вс", "пн", "вт", "ср", "чт", "пт", "сб"],
432
+ ),
433
+
434
+ "ja": DateTimePatterns(
435
+ date_short="y/MM/dd",
436
+ date_medium="y年M月d日",
437
+ date_long="y年M月d日",
438
+ date_full="y年M月d日EEEE",
439
+ time_short="H:mm",
440
+ time_medium="H:mm:ss",
441
+ time_long="H時mm分ss秒 z",
442
+ months_wide=["1月", "2月", "3月", "4月", "5月", "6月",
443
+ "7月", "8月", "9月", "10月", "11月", "12月"],
444
+ months_abbreviated=["1月", "2月", "3月", "4月", "5月", "6月",
445
+ "7月", "8月", "9月", "10月", "11月", "12月"],
446
+ days_wide=["日曜日", "月曜日", "火曜日", "水曜日",
447
+ "木曜日", "金曜日", "土曜日"],
448
+ days_abbreviated=["日", "月", "火", "水", "木", "金", "土"],
449
+ am="午前",
450
+ pm="午後",
451
+ ),
452
+
453
+ "ko": DateTimePatterns(
454
+ date_short="yy. M. d.",
455
+ date_medium="y년 M월 d일",
456
+ date_long="y년 M월 d일",
457
+ date_full="y년 M월 d일 EEEE",
458
+ time_short="a h:mm",
459
+ time_medium="a h:mm:ss",
460
+ months_wide=["1월", "2월", "3월", "4월", "5월", "6월",
461
+ "7월", "8월", "9월", "10월", "11월", "12월"],
462
+ months_abbreviated=["1월", "2월", "3월", "4월", "5월", "6월",
463
+ "7월", "8월", "9월", "10월", "11월", "12월"],
464
+ days_wide=["일요일", "월요일", "화요일", "수요일",
465
+ "목요일", "금요일", "토요일"],
466
+ days_abbreviated=["일", "월", "화", "수", "목", "금", "토"],
467
+ am="오전",
468
+ pm="오후",
469
+ ),
470
+
471
+ "zh": DateTimePatterns(
472
+ date_short="y/M/d",
473
+ date_medium="y年M月d日",
474
+ date_long="y年M月d日",
475
+ date_full="y年M月d日EEEE",
476
+ time_short="HH:mm",
477
+ time_medium="HH:mm:ss",
478
+ months_wide=["一月", "二月", "三月", "四月", "五月", "六月",
479
+ "七月", "八月", "九月", "十月", "十一月", "十二月"],
480
+ months_abbreviated=["1月", "2月", "3月", "4月", "5月", "6月",
481
+ "7月", "8月", "9月", "10月", "11月", "12月"],
482
+ days_wide=["星期日", "星期一", "星期二", "星期三",
483
+ "星期四", "星期五", "星期六"],
484
+ days_abbreviated=["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
485
+ am="上午",
486
+ pm="下午",
487
+ ),
488
+
489
+ "ar": DateTimePatterns(
490
+ date_short="d/M/y",
491
+ date_medium="dd/MM/y",
492
+ date_long="d MMMM y",
493
+ date_full="EEEE، d MMMM y",
494
+ time_short="h:mm a",
495
+ time_medium="h:mm:ss a",
496
+ months_wide=[
497
+ "يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو",
498
+ "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"
499
+ ],
500
+ months_abbreviated=[
501
+ "يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو",
502
+ "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"
503
+ ],
504
+ days_wide=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء",
505
+ "الخميس", "الجمعة", "السبت"],
506
+ days_abbreviated=["أحد", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"],
507
+ am="ص",
508
+ pm="م",
509
+ ),
510
+
511
+ "he": DateTimePatterns(
512
+ date_short="d.M.y",
513
+ date_medium="d בMMM y",
514
+ date_long="d בMMMM y",
515
+ date_full="EEEE, d בMMMM y",
516
+ time_short="H:mm",
517
+ time_medium="H:mm:ss",
518
+ months_wide=[
519
+ "ינואר", "פברואר", "מרץ", "אפריל", "מאי", "יוני",
520
+ "יולי", "אוגוסט", "ספטמבר", "אוקטובר", "נובמבר", "דצמבר"
521
+ ],
522
+ months_abbreviated=[
523
+ "ינו׳", "פבר׳", "מרץ", "אפר׳", "מאי", "יוני",
524
+ "יולי", "אוג׳", "ספט׳", "אוק׳", "נוב׳", "דצמ׳"
525
+ ],
526
+ days_wide=["יום ראשון", "יום שני", "יום שלישי", "יום רביעי",
527
+ "יום חמישי", "יום שישי", "יום שבת"],
528
+ days_abbreviated=["יום א׳", "יום ב׳", "יום ג׳", "יום ד׳", "יום ה׳", "יום ו׳", "שבת"],
529
+ ),
530
+ }
531
+
532
+
533
+ def get_date_patterns(locale: LocaleInfo) -> DateTimePatterns:
534
+ """Get date/time patterns for a locale.
535
+
536
+ Args:
537
+ locale: Target locale
538
+
539
+ Returns:
540
+ DateTimePatterns for the locale
541
+ """
542
+ key = f"{locale.language}_{locale.region}" if locale.region else locale.language
543
+ if key in _DATE_PATTERNS:
544
+ return _DATE_PATTERNS[key]
545
+
546
+ if locale.language in _DATE_PATTERNS:
547
+ return _DATE_PATTERNS[locale.language]
548
+
549
+ return _DATE_PATTERNS["en"]
550
+
551
+
552
+ # ==============================================================================
553
+ # Relative Time Data
554
+ # ==============================================================================
555
+
556
+ @dataclass
557
+ class RelativeTimeUnit:
558
+ """Relative time unit patterns."""
559
+ past_one: str
560
+ past_other: str
561
+ future_one: str
562
+ future_other: str
563
+
564
+
565
+ @dataclass
566
+ class RelativeTimeData:
567
+ """Locale-specific relative time patterns."""
568
+ now: str = "now"
569
+ second: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
570
+ "{0} second ago", "{0} seconds ago", "in {0} second", "in {0} seconds"
571
+ ))
572
+ minute: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
573
+ "{0} minute ago", "{0} minutes ago", "in {0} minute", "in {0} minutes"
574
+ ))
575
+ hour: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
576
+ "{0} hour ago", "{0} hours ago", "in {0} hour", "in {0} hours"
577
+ ))
578
+ day: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
579
+ "{0} day ago", "{0} days ago", "in {0} day", "in {0} days"
580
+ ))
581
+ week: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
582
+ "{0} week ago", "{0} weeks ago", "in {0} week", "in {0} weeks"
583
+ ))
584
+ month: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
585
+ "{0} month ago", "{0} months ago", "in {0} month", "in {0} months"
586
+ ))
587
+ year: RelativeTimeUnit = field(default_factory=lambda: RelativeTimeUnit(
588
+ "{0} year ago", "{0} years ago", "in {0} year", "in {0} years"
589
+ ))
590
+ yesterday: str = "yesterday"
591
+ today: str = "today"
592
+ tomorrow: str = "tomorrow"
593
+
594
+
595
+ _RELATIVE_TIME: dict[str, RelativeTimeData] = {
596
+ "en": RelativeTimeData(),
597
+
598
+ "ko": RelativeTimeData(
599
+ now="지금",
600
+ second=RelativeTimeUnit("{0}초 전", "{0}초 전", "{0}초 후", "{0}초 후"),
601
+ minute=RelativeTimeUnit("{0}분 전", "{0}분 전", "{0}분 후", "{0}분 후"),
602
+ hour=RelativeTimeUnit("{0}시간 전", "{0}시간 전", "{0}시간 후", "{0}시간 후"),
603
+ day=RelativeTimeUnit("{0}일 전", "{0}일 전", "{0}일 후", "{0}일 후"),
604
+ week=RelativeTimeUnit("{0}주 전", "{0}주 전", "{0}주 후", "{0}주 후"),
605
+ month=RelativeTimeUnit("{0}개월 전", "{0}개월 전", "{0}개월 후", "{0}개월 후"),
606
+ year=RelativeTimeUnit("{0}년 전", "{0}년 전", "{0}년 후", "{0}년 후"),
607
+ yesterday="어제",
608
+ today="오늘",
609
+ tomorrow="내일",
610
+ ),
611
+
612
+ "ja": RelativeTimeData(
613
+ now="今",
614
+ second=RelativeTimeUnit("{0}秒前", "{0}秒前", "{0}秒後", "{0}秒後"),
615
+ minute=RelativeTimeUnit("{0}分前", "{0}分前", "{0}分後", "{0}分後"),
616
+ hour=RelativeTimeUnit("{0}時間前", "{0}時間前", "{0}時間後", "{0}時間後"),
617
+ day=RelativeTimeUnit("{0}日前", "{0}日前", "{0}日後", "{0}日後"),
618
+ week=RelativeTimeUnit("{0}週間前", "{0}週間前", "{0}週間後", "{0}週間後"),
619
+ month=RelativeTimeUnit("{0}か月前", "{0}か月前", "{0}か月後", "{0}か月後"),
620
+ year=RelativeTimeUnit("{0}年前", "{0}年前", "{0}年後", "{0}年後"),
621
+ yesterday="昨日",
622
+ today="今日",
623
+ tomorrow="明日",
624
+ ),
625
+
626
+ "zh": RelativeTimeData(
627
+ now="现在",
628
+ second=RelativeTimeUnit("{0}秒钟前", "{0}秒钟前", "{0}秒钟后", "{0}秒钟后"),
629
+ minute=RelativeTimeUnit("{0}分钟前", "{0}分钟前", "{0}分钟后", "{0}分钟后"),
630
+ hour=RelativeTimeUnit("{0}小时前", "{0}小时前", "{0}小时后", "{0}小时后"),
631
+ day=RelativeTimeUnit("{0}天前", "{0}天前", "{0}天后", "{0}天后"),
632
+ week=RelativeTimeUnit("{0}周前", "{0}周前", "{0}周后", "{0}周后"),
633
+ month=RelativeTimeUnit("{0}个月前", "{0}个月前", "{0}个月后", "{0}个月后"),
634
+ year=RelativeTimeUnit("{0}年前", "{0}年前", "{0}年后", "{0}年后"),
635
+ yesterday="昨天",
636
+ today="今天",
637
+ tomorrow="明天",
638
+ ),
639
+
640
+ "de": RelativeTimeData(
641
+ now="jetzt",
642
+ second=RelativeTimeUnit("vor {0} Sekunde", "vor {0} Sekunden", "in {0} Sekunde", "in {0} Sekunden"),
643
+ minute=RelativeTimeUnit("vor {0} Minute", "vor {0} Minuten", "in {0} Minute", "in {0} Minuten"),
644
+ hour=RelativeTimeUnit("vor {0} Stunde", "vor {0} Stunden", "in {0} Stunde", "in {0} Stunden"),
645
+ day=RelativeTimeUnit("vor {0} Tag", "vor {0} Tagen", "in {0} Tag", "in {0} Tagen"),
646
+ week=RelativeTimeUnit("vor {0} Woche", "vor {0} Wochen", "in {0} Woche", "in {0} Wochen"),
647
+ month=RelativeTimeUnit("vor {0} Monat", "vor {0} Monaten", "in {0} Monat", "in {0} Monaten"),
648
+ year=RelativeTimeUnit("vor {0} Jahr", "vor {0} Jahren", "in {0} Jahr", "in {0} Jahren"),
649
+ yesterday="gestern",
650
+ today="heute",
651
+ tomorrow="morgen",
652
+ ),
653
+
654
+ "fr": RelativeTimeData(
655
+ now="maintenant",
656
+ second=RelativeTimeUnit("il y a {0} seconde", "il y a {0} secondes", "dans {0} seconde", "dans {0} secondes"),
657
+ minute=RelativeTimeUnit("il y a {0} minute", "il y a {0} minutes", "dans {0} minute", "dans {0} minutes"),
658
+ hour=RelativeTimeUnit("il y a {0} heure", "il y a {0} heures", "dans {0} heure", "dans {0} heures"),
659
+ day=RelativeTimeUnit("il y a {0} jour", "il y a {0} jours", "dans {0} jour", "dans {0} jours"),
660
+ week=RelativeTimeUnit("il y a {0} semaine", "il y a {0} semaines", "dans {0} semaine", "dans {0} semaines"),
661
+ month=RelativeTimeUnit("il y a {0} mois", "il y a {0} mois", "dans {0} mois", "dans {0} mois"),
662
+ year=RelativeTimeUnit("il y a {0} an", "il y a {0} ans", "dans {0} an", "dans {0} ans"),
663
+ yesterday="hier",
664
+ today="aujourd'hui",
665
+ tomorrow="demain",
666
+ ),
667
+
668
+ "es": RelativeTimeData(
669
+ now="ahora",
670
+ second=RelativeTimeUnit("hace {0} segundo", "hace {0} segundos", "dentro de {0} segundo", "dentro de {0} segundos"),
671
+ minute=RelativeTimeUnit("hace {0} minuto", "hace {0} minutos", "dentro de {0} minuto", "dentro de {0} minutos"),
672
+ hour=RelativeTimeUnit("hace {0} hora", "hace {0} horas", "dentro de {0} hora", "dentro de {0} horas"),
673
+ day=RelativeTimeUnit("hace {0} día", "hace {0} días", "dentro de {0} día", "dentro de {0} días"),
674
+ week=RelativeTimeUnit("hace {0} semana", "hace {0} semanas", "dentro de {0} semana", "dentro de {0} semanas"),
675
+ month=RelativeTimeUnit("hace {0} mes", "hace {0} meses", "dentro de {0} mes", "dentro de {0} meses"),
676
+ year=RelativeTimeUnit("hace {0} año", "hace {0} años", "dentro de {0} año", "dentro de {0} años"),
677
+ yesterday="ayer",
678
+ today="hoy",
679
+ tomorrow="mañana",
680
+ ),
681
+
682
+ "ru": RelativeTimeData(
683
+ now="сейчас",
684
+ second=RelativeTimeUnit("{0} секунду назад", "{0} секунд назад", "через {0} секунду", "через {0} секунд"),
685
+ minute=RelativeTimeUnit("{0} минуту назад", "{0} минут назад", "через {0} минуту", "через {0} минут"),
686
+ hour=RelativeTimeUnit("{0} час назад", "{0} часов назад", "через {0} час", "через {0} часов"),
687
+ day=RelativeTimeUnit("{0} день назад", "{0} дней назад", "через {0} день", "через {0} дней"),
688
+ week=RelativeTimeUnit("{0} неделю назад", "{0} недель назад", "через {0} неделю", "через {0} недель"),
689
+ month=RelativeTimeUnit("{0} месяц назад", "{0} месяцев назад", "через {0} месяц", "через {0} месяцев"),
690
+ year=RelativeTimeUnit("{0} год назад", "{0} лет назад", "через {0} год", "через {0} лет"),
691
+ yesterday="вчера",
692
+ today="сегодня",
693
+ tomorrow="завтра",
694
+ ),
695
+
696
+ "ar": RelativeTimeData(
697
+ now="الآن",
698
+ second=RelativeTimeUnit("قبل {0} ثانية", "قبل {0} ثوانٍ", "خلال {0} ثانية", "خلال {0} ثوانٍ"),
699
+ minute=RelativeTimeUnit("قبل {0} دقيقة", "قبل {0} دقائق", "خلال {0} دقيقة", "خلال {0} دقائق"),
700
+ hour=RelativeTimeUnit("قبل {0} ساعة", "قبل {0} ساعات", "خلال {0} ساعة", "خلال {0} ساعات"),
701
+ day=RelativeTimeUnit("قبل {0} يوم", "قبل {0} أيام", "خلال {0} يوم", "خلال {0} أيام"),
702
+ week=RelativeTimeUnit("قبل {0} أسبوع", "قبل {0} أسابيع", "خلال {0} أسبوع", "خلال {0} أسابيع"),
703
+ month=RelativeTimeUnit("قبل {0} شهر", "قبل {0} أشهر", "خلال {0} شهر", "خلال {0} أشهر"),
704
+ year=RelativeTimeUnit("قبل {0} سنة", "قبل {0} سنوات", "خلال {0} سنة", "خلال {0} سنوات"),
705
+ yesterday="أمس",
706
+ today="اليوم",
707
+ tomorrow="غدًا",
708
+ ),
709
+ }
710
+
711
+
712
+ def get_relative_time_data(locale: LocaleInfo) -> RelativeTimeData:
713
+ """Get relative time patterns for a locale."""
714
+ key = f"{locale.language}_{locale.region}" if locale.region else locale.language
715
+ if key in _RELATIVE_TIME:
716
+ return _RELATIVE_TIME[key]
717
+
718
+ if locale.language in _RELATIVE_TIME:
719
+ return _RELATIVE_TIME[locale.language]
720
+
721
+ return _RELATIVE_TIME["en"]
722
+
723
+
724
+ # ==============================================================================
725
+ # Number Formatter Implementation
726
+ # ==============================================================================
727
+
728
+ class LocaleNumberFormatter(BaseNumberFormatter):
729
+ """Locale-aware number formatter.
730
+
731
+ Supports multiple formatting styles and locale-specific symbols.
732
+
733
+ Example:
734
+ formatter = LocaleNumberFormatter()
735
+
736
+ # Decimal formatting
737
+ formatter.format(1234567.89, LocaleInfo.parse("de"))
738
+ # -> FormattedNumber(formatted="1.234.567,89")
739
+
740
+ # Currency formatting
741
+ formatter.format(1234.56, LocaleInfo.parse("en"), NumberStyle.CURRENCY, currency="USD")
742
+ # -> FormattedNumber(formatted="$1,234.56")
743
+
744
+ # Percent formatting
745
+ formatter.format(0.1234, LocaleInfo.parse("en"), NumberStyle.PERCENT)
746
+ # -> FormattedNumber(formatted="12.34%")
747
+ """
748
+
749
+ def __init__(self, default_precision: int = 2) -> None:
750
+ """Initialize formatter.
751
+
752
+ Args:
753
+ default_precision: Default decimal precision
754
+ """
755
+ self.default_precision = default_precision
756
+
757
+ def format(
758
+ self,
759
+ value: float | int | Decimal,
760
+ locale: LocaleInfo,
761
+ style: NumberStyle = NumberStyle.DECIMAL,
762
+ **options: Any,
763
+ ) -> FormattedNumber:
764
+ """Format a number according to locale rules.
765
+
766
+ Args:
767
+ value: Number to format
768
+ locale: Target locale
769
+ style: Formatting style
770
+ **options: Additional options:
771
+ - precision: Decimal places
772
+ - currency: Currency code (for CURRENCY style)
773
+ - use_grouping: Whether to use grouping separators
774
+ - min_fraction_digits: Minimum fraction digits
775
+ - max_fraction_digits: Maximum fraction digits
776
+ - compact_display: "short" or "long" (for COMPACT style)
777
+
778
+ Returns:
779
+ Formatted number result
780
+ """
781
+ symbols = get_number_symbols(locale)
782
+ direction = locale.direction
783
+
784
+ precision = options.get("precision", self.default_precision)
785
+ use_grouping = options.get("use_grouping", True)
786
+
787
+ if style == NumberStyle.CURRENCY:
788
+ return self._format_currency(value, locale, symbols, direction, **options)
789
+ elif style == NumberStyle.PERCENT:
790
+ return self._format_percent(value, locale, symbols, direction, precision)
791
+ elif style == NumberStyle.SCIENTIFIC:
792
+ return self._format_scientific(value, locale, symbols, direction, precision)
793
+ elif style == NumberStyle.COMPACT:
794
+ return self._format_compact(value, locale, symbols, direction, **options)
795
+ elif style == NumberStyle.ORDINAL:
796
+ return self._format_ordinal(value, locale)
797
+ else:
798
+ return self._format_decimal(value, locale, symbols, direction, precision, use_grouping)
799
+
800
+ def _format_decimal(
801
+ self,
802
+ value: float | int | Decimal,
803
+ locale: LocaleInfo,
804
+ symbols: NumberSymbols,
805
+ direction: TextDirection,
806
+ precision: int,
807
+ use_grouping: bool,
808
+ ) -> FormattedNumber:
809
+ """Format as decimal number."""
810
+ # Handle special values
811
+ if math.isnan(float(value)):
812
+ return FormattedNumber(value=value, formatted=symbols.nan, direction=direction)
813
+ if math.isinf(float(value)):
814
+ sign = "" if value > 0 else symbols.minus
815
+ return FormattedNumber(value=value, formatted=f"{sign}{symbols.infinity}", direction=direction)
816
+
817
+ # Round and format
818
+ if isinstance(value, int):
819
+ int_part = str(abs(value))
820
+ frac_part = ""
821
+ else:
822
+ # Round to precision
823
+ rounded = round(float(value), precision)
824
+ parts = f"{abs(rounded):.{precision}f}".split(".")
825
+ int_part = parts[0]
826
+ frac_part = parts[1] if len(parts) > 1 else ""
827
+
828
+ # Apply grouping
829
+ if use_grouping and len(int_part) > 3:
830
+ int_part = self._apply_grouping(int_part, symbols.group, locale)
831
+
832
+ # Build formatted string
833
+ if frac_part:
834
+ formatted = f"{int_part}{symbols.decimal}{frac_part}"
835
+ else:
836
+ formatted = int_part
837
+
838
+ # Add sign
839
+ if value < 0:
840
+ formatted = f"{symbols.minus}{formatted}"
841
+
842
+ return FormattedNumber(
843
+ value=value,
844
+ formatted=formatted,
845
+ direction=direction,
846
+ parts={"integer": int_part, "decimal": frac_part},
847
+ )
848
+
849
+ def _apply_grouping(self, int_part: str, group_sep: str, locale: LocaleInfo) -> str:
850
+ """Apply grouping separators to integer part."""
851
+ # Indian numbering system uses 2,2,3 grouping
852
+ if locale.language == "hi" or (locale.language == "en" and locale.region == "IN"):
853
+ # First group of 3, then groups of 2
854
+ if len(int_part) <= 3:
855
+ return int_part
856
+ result = int_part[-3:]
857
+ remaining = int_part[:-3]
858
+ while remaining:
859
+ result = remaining[-2:] + group_sep + result
860
+ remaining = remaining[:-2]
861
+ return result
862
+
863
+ # Standard 3-digit grouping
864
+ groups = []
865
+ while len(int_part) > 3:
866
+ groups.insert(0, int_part[-3:])
867
+ int_part = int_part[:-3]
868
+ groups.insert(0, int_part)
869
+ return group_sep.join(groups)
870
+
871
+ def _format_currency(
872
+ self,
873
+ value: float | int | Decimal,
874
+ locale: LocaleInfo,
875
+ symbols: NumberSymbols,
876
+ direction: TextDirection,
877
+ **options: Any,
878
+ ) -> FormattedNumber:
879
+ """Format as currency."""
880
+ currency_code = options.get("currency", "USD")
881
+ currency_info = get_currency_info(currency_code)
882
+ precision = currency_info.decimal_digits
883
+
884
+ # Format the number part
885
+ decimal_result = self._format_decimal(
886
+ value, locale, symbols, direction, precision, True
887
+ )
888
+
889
+ # Get currency symbol
890
+ use_narrow = options.get("narrow", False)
891
+ symbol = currency_info.narrow_symbol if use_narrow else currency_info.symbol
892
+
893
+ # Determine symbol position based on locale
894
+ # Most European locales put symbol after, English-speaking before
895
+ symbol_after = locale.language in ("de", "fr", "es", "it", "pt", "nl", "pl", "cs", "ru", "da", "sv", "no", "fi")
896
+
897
+ if symbol_after:
898
+ formatted = f"{decimal_result.formatted} {symbol}"
899
+ else:
900
+ formatted = f"{symbol}{decimal_result.formatted}"
901
+
902
+ return FormattedNumber(
903
+ value=value,
904
+ formatted=formatted,
905
+ direction=direction,
906
+ parts={"symbol": symbol, "amount": decimal_result.formatted},
907
+ )
908
+
909
+ def _format_percent(
910
+ self,
911
+ value: float | int | Decimal,
912
+ locale: LocaleInfo,
913
+ symbols: NumberSymbols,
914
+ direction: TextDirection,
915
+ precision: int,
916
+ ) -> FormattedNumber:
917
+ """Format as percentage."""
918
+ percent_value = float(value) * 100
919
+ decimal_result = self._format_decimal(
920
+ percent_value, locale, symbols, direction, precision, False
921
+ )
922
+
923
+ # Some locales put space before percent sign
924
+ space = " " if locale.language in ("fr", "de", "ru") else ""
925
+ formatted = f"{decimal_result.formatted}{space}{symbols.percent}"
926
+
927
+ return FormattedNumber(
928
+ value=value,
929
+ formatted=formatted,
930
+ direction=direction,
931
+ )
932
+
933
+ def _format_scientific(
934
+ self,
935
+ value: float | int | Decimal,
936
+ locale: LocaleInfo,
937
+ symbols: NumberSymbols,
938
+ direction: TextDirection,
939
+ precision: int,
940
+ ) -> FormattedNumber:
941
+ """Format in scientific notation."""
942
+ formatted = f"{float(value):.{precision}e}"
943
+ # Replace E with locale symbol
944
+ formatted = formatted.replace("e", symbols.exponential)
945
+ # Replace decimal point
946
+ formatted = formatted.replace(".", symbols.decimal)
947
+
948
+ return FormattedNumber(
949
+ value=value,
950
+ formatted=formatted,
951
+ direction=direction,
952
+ )
953
+
954
+ def _format_compact(
955
+ self,
956
+ value: float | int | Decimal,
957
+ locale: LocaleInfo,
958
+ symbols: NumberSymbols,
959
+ direction: TextDirection,
960
+ **options: Any,
961
+ ) -> FormattedNumber:
962
+ """Format in compact notation (1K, 1M, etc.)."""
963
+ abs_value = abs(float(value))
964
+
965
+ # Define suffixes by locale
966
+ suffixes_en = [
967
+ (1e12, "T"), (1e9, "B"), (1e6, "M"), (1e3, "K")
968
+ ]
969
+ suffixes_ko = [
970
+ (1e12, "조"), (1e8, "억"), (1e4, "만")
971
+ ]
972
+ suffixes_ja = [
973
+ (1e12, "兆"), (1e8, "億"), (1e4, "万")
974
+ ]
975
+ suffixes_zh = [
976
+ (1e12, "万亿"), (1e8, "亿"), (1e4, "万")
977
+ ]
978
+
979
+ if locale.language == "ko":
980
+ suffixes = suffixes_ko
981
+ elif locale.language == "ja":
982
+ suffixes = suffixes_ja
983
+ elif locale.language == "zh":
984
+ suffixes = suffixes_zh
985
+ else:
986
+ suffixes = suffixes_en
987
+
988
+ for threshold, suffix in suffixes:
989
+ if abs_value >= threshold:
990
+ compact_value = float(value) / threshold
991
+ formatted = f"{compact_value:.1f}".rstrip("0").rstrip(".")
992
+ formatted = formatted.replace(".", symbols.decimal)
993
+ if float(value) < 0:
994
+ formatted = f"{symbols.minus}{formatted}"
995
+ return FormattedNumber(
996
+ value=value,
997
+ formatted=f"{formatted}{suffix}",
998
+ direction=direction,
999
+ )
1000
+
1001
+ # No compaction needed
1002
+ return self._format_decimal(value, locale, symbols, direction, 0, True)
1003
+
1004
+ def _format_ordinal(
1005
+ self,
1006
+ value: float | int | Decimal,
1007
+ locale: LocaleInfo,
1008
+ ) -> FormattedNumber:
1009
+ """Format as ordinal number."""
1010
+ n = int(value)
1011
+
1012
+ if locale.language == "en":
1013
+ # English ordinals: 1st, 2nd, 3rd, 4th...
1014
+ if 11 <= n % 100 <= 13:
1015
+ suffix = "th"
1016
+ else:
1017
+ suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
1018
+ formatted = f"{n}{suffix}"
1019
+
1020
+ elif locale.language == "ko":
1021
+ formatted = f"제{n}"
1022
+
1023
+ elif locale.language == "ja":
1024
+ formatted = f"第{n}"
1025
+
1026
+ elif locale.language == "zh":
1027
+ formatted = f"第{n}"
1028
+
1029
+ elif locale.language in ("de", "nl", "da", "sv", "no"):
1030
+ # German-style: add period
1031
+ formatted = f"{n}."
1032
+
1033
+ elif locale.language in ("fr", "es", "it", "pt"):
1034
+ # Romance languages with gender
1035
+ if n == 1:
1036
+ formatted = "1ᵉʳ" if locale.language == "fr" else f"{n}º"
1037
+ else:
1038
+ formatted = f"{n}ᵉ" if locale.language == "fr" else f"{n}º"
1039
+
1040
+ elif locale.language == "ru":
1041
+ formatted = f"{n}-й"
1042
+
1043
+ else:
1044
+ formatted = str(n)
1045
+
1046
+ return FormattedNumber(
1047
+ value=value,
1048
+ formatted=formatted,
1049
+ direction=locale.direction,
1050
+ )
1051
+
1052
+ def parse(
1053
+ self,
1054
+ text: str,
1055
+ locale: LocaleInfo,
1056
+ style: NumberStyle = NumberStyle.DECIMAL,
1057
+ ) -> float | int | Decimal | None:
1058
+ """Parse a localized number string."""
1059
+ symbols = get_number_symbols(locale)
1060
+
1061
+ # Clean the text
1062
+ cleaned = text.strip()
1063
+
1064
+ # Remove grouping separators
1065
+ cleaned = cleaned.replace(symbols.group, "")
1066
+
1067
+ # Normalize decimal separator
1068
+ cleaned = cleaned.replace(symbols.decimal, ".")
1069
+
1070
+ # Handle percent
1071
+ if style == NumberStyle.PERCENT:
1072
+ cleaned = cleaned.replace(symbols.percent, "").strip()
1073
+ try:
1074
+ return float(cleaned) / 100
1075
+ except ValueError:
1076
+ return None
1077
+
1078
+ # Handle currency symbols
1079
+ if style == NumberStyle.CURRENCY:
1080
+ # Remove common currency symbols
1081
+ for symbol in ["$", "€", "£", "¥", "₩", "₹"]:
1082
+ cleaned = cleaned.replace(symbol, "")
1083
+ cleaned = cleaned.strip()
1084
+
1085
+ try:
1086
+ # Try integer first
1087
+ if "." not in cleaned:
1088
+ return int(cleaned)
1089
+ return float(cleaned)
1090
+ except ValueError:
1091
+ return None
1092
+
1093
+
1094
+ # ==============================================================================
1095
+ # Date Formatter Implementation
1096
+ # ==============================================================================
1097
+
1098
+ class LocaleDateFormatter(BaseDateFormatter):
1099
+ """Locale-aware date/time formatter.
1100
+
1101
+ Supports multiple date styles and relative time formatting.
1102
+
1103
+ Example:
1104
+ formatter = LocaleDateFormatter()
1105
+
1106
+ # Date formatting
1107
+ formatter.format_date(date.today(), LocaleInfo.parse("ko"), DateStyle.LONG)
1108
+ # -> FormattedDate(formatted="2024년 12월 28일")
1109
+
1110
+ # Relative time
1111
+ formatter.format_relative(datetime.now() - timedelta(days=2), locale=LocaleInfo.parse("en"))
1112
+ # -> FormattedDate(formatted="2 days ago")
1113
+ """
1114
+
1115
+ def format_date(
1116
+ self,
1117
+ value: datetime | date,
1118
+ locale: LocaleInfo,
1119
+ style: DateStyle = DateStyle.MEDIUM,
1120
+ **options: Any,
1121
+ ) -> FormattedDate:
1122
+ """Format a date according to locale rules."""
1123
+ if style == DateStyle.ISO:
1124
+ if isinstance(value, datetime):
1125
+ formatted = value.strftime("%Y-%m-%d")
1126
+ else:
1127
+ formatted = value.strftime("%Y-%m-%d")
1128
+ return FormattedDate(value=value, formatted=formatted, direction=locale.direction)
1129
+
1130
+ if style == DateStyle.RELATIVE:
1131
+ return self.format_relative(value, locale=locale)
1132
+
1133
+ patterns = get_date_patterns(locale)
1134
+ direction = locale.direction
1135
+
1136
+ if style == DateStyle.SHORT:
1137
+ formatted = self._apply_pattern(value, patterns.date_short, patterns)
1138
+ elif style == DateStyle.MEDIUM:
1139
+ formatted = self._apply_pattern(value, patterns.date_medium, patterns)
1140
+ elif style == DateStyle.LONG:
1141
+ formatted = self._apply_pattern(value, patterns.date_long, patterns)
1142
+ elif style == DateStyle.FULL:
1143
+ formatted = self._apply_pattern(value, patterns.date_full, patterns)
1144
+ else:
1145
+ formatted = self._apply_pattern(value, patterns.date_medium, patterns)
1146
+
1147
+ return FormattedDate(value=value, formatted=formatted, direction=direction)
1148
+
1149
+ def format_time(
1150
+ self,
1151
+ value: datetime | time,
1152
+ locale: LocaleInfo,
1153
+ style: TimeStyle = TimeStyle.MEDIUM,
1154
+ **options: Any,
1155
+ ) -> FormattedDate:
1156
+ """Format a time according to locale rules."""
1157
+ patterns = get_date_patterns(locale)
1158
+ direction = locale.direction
1159
+
1160
+ if isinstance(value, time):
1161
+ value = datetime.combine(date.today(), value)
1162
+
1163
+ if style == TimeStyle.SHORT:
1164
+ formatted = self._apply_pattern(value, patterns.time_short, patterns)
1165
+ elif style == TimeStyle.MEDIUM:
1166
+ formatted = self._apply_pattern(value, patterns.time_medium, patterns)
1167
+ elif style == TimeStyle.LONG:
1168
+ formatted = self._apply_pattern(value, patterns.time_long, patterns)
1169
+ elif style == TimeStyle.FULL:
1170
+ formatted = self._apply_pattern(value, patterns.time_full, patterns)
1171
+ else:
1172
+ formatted = self._apply_pattern(value, patterns.time_medium, patterns)
1173
+
1174
+ return FormattedDate(value=value, formatted=formatted, direction=direction)
1175
+
1176
+ def _apply_pattern(
1177
+ self,
1178
+ value: datetime | date,
1179
+ pattern: str,
1180
+ patterns: DateTimePatterns,
1181
+ ) -> str:
1182
+ """Apply a date/time pattern to a value."""
1183
+ if isinstance(value, date) and not isinstance(value, datetime):
1184
+ value = datetime.combine(value, time.min)
1185
+
1186
+ result = pattern
1187
+
1188
+ # Year
1189
+ result = result.replace("yyyy", str(value.year))
1190
+ result = result.replace("yyy", str(value.year))
1191
+ result = result.replace("yy", str(value.year)[-2:])
1192
+ result = result.replace("y", str(value.year))
1193
+
1194
+ # Month
1195
+ month_idx = value.month - 1
1196
+ result = result.replace("MMMM", patterns.months_wide[month_idx])
1197
+ result = result.replace("MMM", patterns.months_abbreviated[month_idx])
1198
+ result = result.replace("MM", f"{value.month:02d}")
1199
+ result = result.replace("M", str(value.month))
1200
+
1201
+ # Day
1202
+ result = result.replace("dd", f"{value.day:02d}")
1203
+ result = result.replace("d", str(value.day))
1204
+
1205
+ # Day of week
1206
+ dow_idx = value.weekday() # 0=Monday in Python
1207
+ dow_idx = (dow_idx + 1) % 7 # Convert to 0=Sunday
1208
+ result = result.replace("EEEE", patterns.days_wide[dow_idx])
1209
+ result = result.replace("EEE", patterns.days_abbreviated[dow_idx])
1210
+ result = result.replace("E", patterns.days_abbreviated[dow_idx])
1211
+
1212
+ # Hour (24-hour)
1213
+ result = result.replace("HH", f"{value.hour:02d}")
1214
+ result = result.replace("H", str(value.hour))
1215
+
1216
+ # Hour (12-hour)
1217
+ hour12 = value.hour % 12 or 12
1218
+ result = result.replace("hh", f"{hour12:02d}")
1219
+ result = result.replace("h", str(hour12))
1220
+
1221
+ # Minute
1222
+ result = result.replace("mm", f"{value.minute:02d}")
1223
+ # Don't replace single 'm' as it might be part of AM/PM
1224
+
1225
+ # Second
1226
+ result = result.replace("ss", f"{value.second:02d}")
1227
+ result = result.replace("s", str(value.second))
1228
+
1229
+ # AM/PM
1230
+ am_pm = patterns.am if value.hour < 12 else patterns.pm
1231
+ result = result.replace("a", am_pm)
1232
+
1233
+ # Handle quoted literals (e.g., 'de' in Spanish patterns)
1234
+ result = re.sub(r"'([^']*)'", r"\1", result)
1235
+
1236
+ return result
1237
+
1238
+ def format_relative(
1239
+ self,
1240
+ value: datetime | date,
1241
+ reference: datetime | date | None = None,
1242
+ locale: LocaleInfo | None = None,
1243
+ ) -> FormattedDate:
1244
+ """Format a relative date (e.g., "2 days ago")."""
1245
+ if locale is None:
1246
+ locale = LocaleInfo.parse("en")
1247
+
1248
+ if reference is None:
1249
+ reference = datetime.now()
1250
+
1251
+ if isinstance(value, date) and not isinstance(value, datetime):
1252
+ value = datetime.combine(value, time.min)
1253
+ if isinstance(reference, date) and not isinstance(reference, datetime):
1254
+ reference = datetime.combine(reference, time.min)
1255
+
1256
+ delta = value - reference
1257
+ rel_data = get_relative_time_data(locale)
1258
+
1259
+ # Check for special cases
1260
+ days = delta.days
1261
+ if days == 0 and abs(delta.total_seconds()) < 60:
1262
+ return FormattedDate(value=value, formatted=rel_data.now, direction=locale.direction)
1263
+ if days == 0:
1264
+ pass # Handle hours/minutes below
1265
+ elif days == 1:
1266
+ return FormattedDate(value=value, formatted=rel_data.tomorrow, direction=locale.direction)
1267
+ elif days == -1:
1268
+ return FormattedDate(value=value, formatted=rel_data.yesterday, direction=locale.direction)
1269
+
1270
+ # Calculate appropriate unit
1271
+ total_seconds = abs(delta.total_seconds())
1272
+ is_future = delta.total_seconds() > 0
1273
+
1274
+ if total_seconds < 60:
1275
+ n = int(total_seconds)
1276
+ unit = rel_data.second
1277
+ elif total_seconds < 3600:
1278
+ n = int(total_seconds / 60)
1279
+ unit = rel_data.minute
1280
+ elif total_seconds < 86400:
1281
+ n = int(total_seconds / 3600)
1282
+ unit = rel_data.hour
1283
+ elif total_seconds < 604800:
1284
+ n = abs(days)
1285
+ unit = rel_data.day
1286
+ elif total_seconds < 2592000: # ~30 days
1287
+ n = abs(days) // 7
1288
+ unit = rel_data.week
1289
+ elif total_seconds < 31536000: # ~365 days
1290
+ n = abs(days) // 30
1291
+ unit = rel_data.month
1292
+ else:
1293
+ n = abs(days) // 365
1294
+ unit = rel_data.year
1295
+
1296
+ # Select pattern based on plurality and direction
1297
+ if is_future:
1298
+ pattern = unit.future_one if n == 1 else unit.future_other
1299
+ else:
1300
+ pattern = unit.past_one if n == 1 else unit.past_other
1301
+
1302
+ formatted = pattern.format(n)
1303
+
1304
+ return FormattedDate(value=value, formatted=formatted, direction=locale.direction)
1305
+
1306
+
1307
+ # ==============================================================================
1308
+ # Convenience Functions
1309
+ # ==============================================================================
1310
+
1311
+ _number_formatter = LocaleNumberFormatter()
1312
+ _date_formatter = LocaleDateFormatter()
1313
+
1314
+
1315
+ def format_number(
1316
+ value: float | int | Decimal,
1317
+ locale: str | LocaleInfo,
1318
+ style: NumberStyle = NumberStyle.DECIMAL,
1319
+ **options: Any,
1320
+ ) -> str:
1321
+ """Format a number for a locale.
1322
+
1323
+ Args:
1324
+ value: Number to format
1325
+ locale: Target locale (string or LocaleInfo)
1326
+ style: Formatting style
1327
+ **options: Additional formatting options
1328
+
1329
+ Returns:
1330
+ Formatted number string
1331
+
1332
+ Example:
1333
+ format_number(1234567.89, "de") # "1.234.567,89"
1334
+ format_number(0.15, "en", NumberStyle.PERCENT) # "15.00%"
1335
+ """
1336
+ if isinstance(locale, str):
1337
+ locale = LocaleInfo.parse(locale)
1338
+ return _number_formatter.format(value, locale, style, **options).formatted
1339
+
1340
+
1341
+ def format_currency(
1342
+ value: float | int | Decimal,
1343
+ currency: str,
1344
+ locale: str | LocaleInfo,
1345
+ **options: Any,
1346
+ ) -> str:
1347
+ """Format a currency value.
1348
+
1349
+ Args:
1350
+ value: Amount
1351
+ currency: ISO 4217 currency code
1352
+ locale: Target locale
1353
+
1354
+ Returns:
1355
+ Formatted currency string
1356
+
1357
+ Example:
1358
+ format_currency(1234.56, "USD", "en") # "$1,234.56"
1359
+ format_currency(1234.56, "EUR", "de") # "1.234,56 €"
1360
+ """
1361
+ if isinstance(locale, str):
1362
+ locale = LocaleInfo.parse(locale)
1363
+ return _number_formatter.format(
1364
+ value, locale, NumberStyle.CURRENCY, currency=currency, **options
1365
+ ).formatted
1366
+
1367
+
1368
+ def format_date(
1369
+ value: datetime | date,
1370
+ locale: str | LocaleInfo,
1371
+ style: DateStyle = DateStyle.MEDIUM,
1372
+ **options: Any,
1373
+ ) -> str:
1374
+ """Format a date for a locale.
1375
+
1376
+ Args:
1377
+ value: Date to format
1378
+ locale: Target locale
1379
+ style: Formatting style
1380
+
1381
+ Returns:
1382
+ Formatted date string
1383
+
1384
+ Example:
1385
+ format_date(date.today(), "ko", DateStyle.LONG) # "2024년 12월 28일"
1386
+ """
1387
+ if isinstance(locale, str):
1388
+ locale = LocaleInfo.parse(locale)
1389
+ return _date_formatter.format_date(value, locale, style, **options).formatted
1390
+
1391
+
1392
+ def format_time(
1393
+ value: datetime | time,
1394
+ locale: str | LocaleInfo,
1395
+ style: TimeStyle = TimeStyle.MEDIUM,
1396
+ **options: Any,
1397
+ ) -> str:
1398
+ """Format a time for a locale.
1399
+
1400
+ Args:
1401
+ value: Time to format
1402
+ locale: Target locale
1403
+ style: Formatting style
1404
+
1405
+ Returns:
1406
+ Formatted time string
1407
+ """
1408
+ if isinstance(locale, str):
1409
+ locale = LocaleInfo.parse(locale)
1410
+ return _date_formatter.format_time(value, locale, style, **options).formatted
1411
+
1412
+
1413
+ def format_relative_time(
1414
+ value: datetime | date,
1415
+ reference: datetime | date | None = None,
1416
+ locale: str | LocaleInfo = "en",
1417
+ ) -> str:
1418
+ """Format a relative time.
1419
+
1420
+ Args:
1421
+ value: Date/time to format
1422
+ reference: Reference date (default: now)
1423
+ locale: Target locale
1424
+
1425
+ Returns:
1426
+ Relative time string (e.g., "2 days ago")
1427
+
1428
+ Example:
1429
+ format_relative_time(datetime.now() - timedelta(hours=3), locale="en")
1430
+ # -> "3 hours ago"
1431
+ """
1432
+ if isinstance(locale, str):
1433
+ locale = LocaleInfo.parse(locale)
1434
+ return _date_formatter.format_relative(value, reference, locale).formatted