pycharter 0.0.25__py3-none-any.whl → 0.0.26__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 (462) hide show
  1. pycharter/__init__.py +6 -0
  2. pycharter/api/README.md +1 -1
  3. pycharter/api/dependencies/auth.py +158 -0
  4. pycharter/api/main.py +30 -2
  5. pycharter/api/models/etl.py +66 -0
  6. pycharter/api/routes/v1/__init__.py +4 -0
  7. pycharter/api/routes/v1/auth.py +97 -0
  8. pycharter/api/routes/v1/contracts.py +10 -8
  9. pycharter/api/routes/v1/etl.py +131 -0
  10. pycharter/cli.py +1 -1
  11. pycharter/config.py +69 -0
  12. pycharter/contract_builder/builder.py +32 -37
  13. pycharter/data/seed/compliance_frameworks.yaml +22 -0
  14. pycharter/data/seed/contracts.yaml +130 -0
  15. pycharter/data/seed/data_feeds.yaml +22 -0
  16. pycharter/data/seed/domains.yaml +13 -0
  17. pycharter/data/seed/environments.yaml +19 -0
  18. pycharter/data/seed/owners.yaml +21 -0
  19. pycharter/data/seed/systems.yaml +13 -0
  20. pycharter/data/seed/tags.yaml +25 -0
  21. pycharter/data/templates/contract/README.md +31 -14
  22. pycharter/data/templates/contract/template_contract.yaml +37 -0
  23. pycharter/data/templates/etl/README.md +1 -1
  24. pycharter/data/templates/etl/extract_with_validation.yaml +86 -0
  25. pycharter/data/templates/etl/load_with_validation.yaml +111 -0
  26. pycharter/data/templates/etl/settings.yaml +55 -0
  27. pycharter/db/cli.py +126 -4
  28. pycharter/db/migrations/versions/20260122000000_change_artifact_unique_constraints_to_title_version.py +2 -2
  29. pycharter/etl_generator/INTERFACES.md +6 -7
  30. pycharter/etl_generator/__init__.py +47 -11
  31. pycharter/etl_generator/config_models.py +673 -0
  32. pycharter/etl_generator/config_validator.py +133 -157
  33. pycharter/etl_generator/context.py +3 -0
  34. pycharter/etl_generator/database.py +5 -1
  35. pycharter/etl_generator/extractors/__init__.py +4 -2
  36. pycharter/etl_generator/extractors/cloud_storage.py +9 -9
  37. pycharter/etl_generator/extractors/database.py +2 -2
  38. pycharter/etl_generator/extractors/factory.py +15 -33
  39. pycharter/etl_generator/extractors/file.py +2 -2
  40. pycharter/etl_generator/extractors/http.py +2 -2
  41. pycharter/etl_generator/extractors/mongodb.py +393 -0
  42. pycharter/etl_generator/extractors/streaming.py +2 -2
  43. pycharter/etl_generator/loaders/__init__.py +15 -9
  44. pycharter/etl_generator/loaders/{cloud_storage_loader.py → cloud_storage.py} +95 -2
  45. pycharter/etl_generator/loaders/factory.py +16 -29
  46. pycharter/etl_generator/loaders/file.py +135 -1
  47. pycharter/etl_generator/loaders/mongodb.py +416 -0
  48. pycharter/etl_generator/pipeline.py +283 -164
  49. pycharter/etl_generator/result.py +16 -0
  50. pycharter/etl_generator/schemas/__init__.py +71 -42
  51. pycharter/etl_generator/transformers/config.py +3 -2
  52. pycharter/etl_generator/transformers/simple_operations.py +57 -4
  53. pycharter/etl_generator/validation.py +551 -0
  54. pycharter/runtime_validator/__init__.py +7 -0
  55. pycharter/runtime_validator/utils.py +33 -0
  56. pycharter/runtime_validator/validator.py +13 -10
  57. pycharter/ui/package-lock.json +50 -41
  58. pycharter/ui/package.json +2 -1
  59. pycharter/ui/static/404/index.html +1 -1
  60. pycharter/ui/static/404.html +1 -1
  61. pycharter/ui/static/__next.__PAGE__.txt +2 -2
  62. pycharter/ui/static/__next._full.txt +7 -7
  63. pycharter/ui/static/__next._head.txt +1 -1
  64. pycharter/ui/static/__next._index.txt +6 -6
  65. pycharter/ui/static/__next._tree.txt +2 -2
  66. pycharter/ui/static/_next/static/chunks/0fc1f70b787b8845.js +1 -0
  67. pycharter/ui/static/_next/static/chunks/17bb8075d7b75663.css +1 -0
  68. pycharter/ui/static/_next/static/chunks/381932864dcbfdb8.js +1 -0
  69. pycharter/ui/static/_next/static/chunks/4c951b8e4507e2b3.js +1 -0
  70. pycharter/ui/static/_next/static/chunks/68b87a6f65abd3ed.js +1 -0
  71. pycharter/ui/static/_next/static/chunks/78572617b8fae189.js +1 -0
  72. pycharter/ui/static/_next/static/chunks/8b7be2803e3fe184.js +1 -0
  73. pycharter/ui/static/_next/static/chunks/a8e529fd1e67f121.js +1 -0
  74. pycharter/ui/static/_next/static/chunks/c35d998f80be3ff5.js +1 -0
  75. pycharter/ui/static/_next/static/chunks/e453aa5d01c32c17.js +1 -0
  76. pycharter/ui/static/_next/static/chunks/f2d240eb057f898a.js +970 -0
  77. pycharter/ui/static/_next/static/chunks/f7722448f6040846.js +1 -0
  78. pycharter/ui/static/_not-found/__next._full.txt +12 -12
  79. pycharter/ui/static/_not-found/__next._head.txt +3 -3
  80. pycharter/ui/static/_not-found/__next._index.txt +8 -8
  81. pycharter/ui/static/_not-found/__next._not-found.__PAGE__.txt +2 -2
  82. pycharter/ui/static/_not-found/__next._not-found.txt +3 -3
  83. pycharter/ui/static/_not-found/__next._tree.txt +2 -2
  84. pycharter/ui/static/_not-found/index.html +1 -1
  85. pycharter/ui/static/_not-found/index.txt +12 -12
  86. pycharter/ui/static/contracts/__next._full.txt +7 -7
  87. pycharter/ui/static/contracts/__next._head.txt +1 -1
  88. pycharter/ui/static/contracts/__next._index.txt +6 -6
  89. pycharter/ui/static/contracts/__next._tree.txt +2 -2
  90. pycharter/ui/static/contracts/__next.contracts.__PAGE__.txt +2 -2
  91. pycharter/ui/static/contracts/__next.contracts.txt +1 -1
  92. pycharter/ui/static/contracts/index.html +1 -1
  93. pycharter/ui/static/contracts/index.txt +7 -7
  94. pycharter/ui/static/documentation/__next._full.txt +7 -7
  95. pycharter/ui/static/documentation/__next._head.txt +1 -1
  96. pycharter/ui/static/documentation/__next._index.txt +6 -6
  97. pycharter/ui/static/documentation/__next._tree.txt +2 -2
  98. pycharter/ui/static/documentation/__next.documentation.__PAGE__.txt +2 -2
  99. pycharter/ui/static/documentation/__next.documentation.txt +1 -1
  100. pycharter/ui/static/documentation/index.html +3 -3
  101. pycharter/ui/static/documentation/index.txt +7 -7
  102. pycharter/ui/static/etl/__next._full.txt +21 -0
  103. pycharter/ui/static/etl/__next._head.txt +7 -0
  104. pycharter/ui/static/etl/__next._index.txt +9 -0
  105. pycharter/ui/static/etl/__next._tree.txt +2 -0
  106. pycharter/ui/static/etl/__next.etl.__PAGE__.txt +9 -0
  107. pycharter/ui/static/etl/__next.etl.txt +4 -0
  108. pycharter/ui/static/etl/index.html +2 -0
  109. pycharter/ui/static/etl/index.txt +21 -0
  110. pycharter/ui/static/index.html +1 -1
  111. pycharter/ui/static/index.txt +7 -7
  112. pycharter/ui/static/metadata/__next._full.txt +7 -7
  113. pycharter/ui/static/metadata/__next._head.txt +1 -1
  114. pycharter/ui/static/metadata/__next._index.txt +6 -6
  115. pycharter/ui/static/metadata/__next._tree.txt +2 -2
  116. pycharter/ui/static/metadata/__next.metadata.__PAGE__.txt +2 -2
  117. pycharter/ui/static/metadata/__next.metadata.txt +1 -1
  118. pycharter/ui/static/metadata/index.html +1 -1
  119. pycharter/ui/static/metadata/index.txt +7 -7
  120. pycharter/ui/static/quality/__next._full.txt +7 -7
  121. pycharter/ui/static/quality/__next._head.txt +1 -1
  122. pycharter/ui/static/quality/__next._index.txt +6 -6
  123. pycharter/ui/static/quality/__next._tree.txt +2 -2
  124. pycharter/ui/static/quality/__next.quality.__PAGE__.txt +2 -2
  125. pycharter/ui/static/quality/__next.quality.txt +1 -1
  126. pycharter/ui/static/quality/index.html +2 -2
  127. pycharter/ui/static/quality/index.txt +7 -7
  128. pycharter/ui/static/rules/__next._full.txt +7 -7
  129. pycharter/ui/static/rules/__next._head.txt +1 -1
  130. pycharter/ui/static/rules/__next._index.txt +6 -6
  131. pycharter/ui/static/rules/__next._tree.txt +2 -2
  132. pycharter/ui/static/rules/__next.rules.__PAGE__.txt +2 -2
  133. pycharter/ui/static/rules/__next.rules.txt +1 -1
  134. pycharter/ui/static/rules/index.html +1 -1
  135. pycharter/ui/static/rules/index.txt +7 -7
  136. pycharter/ui/static/schemas/__next._full.txt +7 -7
  137. pycharter/ui/static/schemas/__next._head.txt +1 -1
  138. pycharter/ui/static/schemas/__next._index.txt +6 -6
  139. pycharter/ui/static/schemas/__next._tree.txt +2 -2
  140. pycharter/ui/static/schemas/__next.schemas.__PAGE__.txt +2 -2
  141. pycharter/ui/static/schemas/__next.schemas.txt +1 -1
  142. pycharter/ui/static/schemas/index.html +1 -1
  143. pycharter/ui/static/schemas/index.txt +7 -7
  144. pycharter/ui/static/settings/__next._full.txt +7 -7
  145. pycharter/ui/static/settings/__next._head.txt +1 -1
  146. pycharter/ui/static/settings/__next._index.txt +6 -6
  147. pycharter/ui/static/settings/__next._tree.txt +2 -2
  148. pycharter/ui/static/settings/__next.settings.__PAGE__.txt +2 -2
  149. pycharter/ui/static/settings/__next.settings.txt +1 -1
  150. pycharter/ui/static/settings/index.html +1 -1
  151. pycharter/ui/static/settings/index.txt +7 -7
  152. pycharter/ui/static/static/404/index.html +1 -1
  153. pycharter/ui/static/static/404.html +1 -1
  154. pycharter/ui/static/static/__next.__PAGE__.txt +1 -1
  155. pycharter/ui/static/static/__next._full.txt +1 -1
  156. pycharter/ui/static/static/__next._head.txt +1 -1
  157. pycharter/ui/static/static/__next._index.txt +1 -1
  158. pycharter/ui/static/static/__next._tree.txt +1 -1
  159. pycharter/ui/static/static/_not-found/__next._full.txt +1 -1
  160. pycharter/ui/static/static/_not-found/__next._head.txt +1 -1
  161. pycharter/ui/static/static/_not-found/__next._index.txt +1 -1
  162. pycharter/ui/static/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
  163. pycharter/ui/static/static/_not-found/__next._not-found.txt +1 -1
  164. pycharter/ui/static/static/_not-found/__next._tree.txt +1 -1
  165. pycharter/ui/static/static/_not-found/index.html +1 -1
  166. pycharter/ui/static/static/_not-found/index.txt +1 -1
  167. pycharter/ui/static/static/contracts/__next._full.txt +2 -2
  168. pycharter/ui/static/static/contracts/__next._head.txt +1 -1
  169. pycharter/ui/static/static/contracts/__next._index.txt +1 -1
  170. pycharter/ui/static/static/contracts/__next._tree.txt +1 -1
  171. pycharter/ui/static/static/contracts/__next.contracts.__PAGE__.txt +2 -2
  172. pycharter/ui/static/static/contracts/__next.contracts.txt +1 -1
  173. pycharter/ui/static/static/contracts/index.html +1 -1
  174. pycharter/ui/static/static/contracts/index.txt +2 -2
  175. pycharter/ui/static/static/documentation/__next._full.txt +1 -1
  176. pycharter/ui/static/static/documentation/__next._head.txt +1 -1
  177. pycharter/ui/static/static/documentation/__next._index.txt +1 -1
  178. pycharter/ui/static/static/documentation/__next._tree.txt +1 -1
  179. pycharter/ui/static/static/documentation/__next.documentation.__PAGE__.txt +1 -1
  180. pycharter/ui/static/static/documentation/__next.documentation.txt +1 -1
  181. pycharter/ui/static/static/documentation/index.html +2 -2
  182. pycharter/ui/static/static/documentation/index.txt +1 -1
  183. pycharter/ui/static/static/index.html +1 -1
  184. pycharter/ui/static/static/index.txt +1 -1
  185. pycharter/ui/static/static/metadata/__next._full.txt +1 -1
  186. pycharter/ui/static/static/metadata/__next._head.txt +1 -1
  187. pycharter/ui/static/static/metadata/__next._index.txt +1 -1
  188. pycharter/ui/static/static/metadata/__next._tree.txt +1 -1
  189. pycharter/ui/static/static/metadata/__next.metadata.__PAGE__.txt +1 -1
  190. pycharter/ui/static/static/metadata/__next.metadata.txt +1 -1
  191. pycharter/ui/static/static/metadata/index.html +1 -1
  192. pycharter/ui/static/static/metadata/index.txt +1 -1
  193. pycharter/ui/static/static/quality/__next._full.txt +2 -2
  194. pycharter/ui/static/static/quality/__next._head.txt +1 -1
  195. pycharter/ui/static/static/quality/__next._index.txt +1 -1
  196. pycharter/ui/static/static/quality/__next._tree.txt +1 -1
  197. pycharter/ui/static/static/quality/__next.quality.__PAGE__.txt +2 -2
  198. pycharter/ui/static/static/quality/__next.quality.txt +1 -1
  199. pycharter/ui/static/static/quality/index.html +2 -2
  200. pycharter/ui/static/static/quality/index.txt +2 -2
  201. pycharter/ui/static/static/rules/__next._full.txt +1 -1
  202. pycharter/ui/static/static/rules/__next._head.txt +1 -1
  203. pycharter/ui/static/static/rules/__next._index.txt +1 -1
  204. pycharter/ui/static/static/rules/__next._tree.txt +1 -1
  205. pycharter/ui/static/static/rules/__next.rules.__PAGE__.txt +1 -1
  206. pycharter/ui/static/static/rules/__next.rules.txt +1 -1
  207. pycharter/ui/static/static/rules/index.html +1 -1
  208. pycharter/ui/static/static/rules/index.txt +1 -1
  209. pycharter/ui/static/static/schemas/__next._full.txt +1 -1
  210. pycharter/ui/static/static/schemas/__next._head.txt +1 -1
  211. pycharter/ui/static/static/schemas/__next._index.txt +1 -1
  212. pycharter/ui/static/static/schemas/__next._tree.txt +1 -1
  213. pycharter/ui/static/static/schemas/__next.schemas.__PAGE__.txt +1 -1
  214. pycharter/ui/static/static/schemas/__next.schemas.txt +1 -1
  215. pycharter/ui/static/static/schemas/index.html +1 -1
  216. pycharter/ui/static/static/schemas/index.txt +1 -1
  217. pycharter/ui/static/static/settings/__next._full.txt +1 -1
  218. pycharter/ui/static/static/settings/__next._head.txt +1 -1
  219. pycharter/ui/static/static/settings/__next._index.txt +1 -1
  220. pycharter/ui/static/static/settings/__next._tree.txt +1 -1
  221. pycharter/ui/static/static/settings/__next.settings.__PAGE__.txt +1 -1
  222. pycharter/ui/static/static/settings/__next.settings.txt +1 -1
  223. pycharter/ui/static/static/settings/index.html +1 -1
  224. pycharter/ui/static/static/settings/index.txt +1 -1
  225. pycharter/ui/static/static/static/404/index.html +1 -1
  226. pycharter/ui/static/static/static/404.html +1 -1
  227. pycharter/ui/static/static/static/__next.__PAGE__.txt +1 -1
  228. pycharter/ui/static/static/static/__next._full.txt +2 -2
  229. pycharter/ui/static/static/static/__next._head.txt +1 -1
  230. pycharter/ui/static/static/static/__next._index.txt +2 -2
  231. pycharter/ui/static/static/static/__next._tree.txt +2 -2
  232. pycharter/ui/static/static/static/_next/static/chunks/f7d1a90dd75d2572.js +1 -0
  233. pycharter/ui/static/static/static/_not-found/__next._full.txt +2 -2
  234. pycharter/ui/static/static/static/_not-found/__next._head.txt +1 -1
  235. pycharter/ui/static/static/static/_not-found/__next._index.txt +2 -2
  236. pycharter/ui/static/static/static/_not-found/__next._not-found.__PAGE__.txt +1 -1
  237. pycharter/ui/static/static/static/_not-found/__next._not-found.txt +1 -1
  238. pycharter/ui/static/static/static/_not-found/__next._tree.txt +2 -2
  239. pycharter/ui/static/static/static/_not-found/index.html +1 -1
  240. pycharter/ui/static/static/static/_not-found/index.txt +2 -2
  241. pycharter/ui/static/static/static/contracts/__next._full.txt +3 -3
  242. pycharter/ui/static/static/static/contracts/__next._head.txt +1 -1
  243. pycharter/ui/static/static/static/contracts/__next._index.txt +2 -2
  244. pycharter/ui/static/static/static/contracts/__next._tree.txt +2 -2
  245. pycharter/ui/static/static/static/contracts/__next.contracts.__PAGE__.txt +2 -2
  246. pycharter/ui/static/static/static/contracts/__next.contracts.txt +1 -1
  247. pycharter/ui/static/static/static/contracts/index.html +1 -1
  248. pycharter/ui/static/static/static/contracts/index.txt +3 -3
  249. pycharter/ui/static/static/static/documentation/__next._full.txt +3 -3
  250. pycharter/ui/static/static/static/documentation/__next._head.txt +1 -1
  251. pycharter/ui/static/static/static/documentation/__next._index.txt +2 -2
  252. pycharter/ui/static/static/static/documentation/__next._tree.txt +2 -2
  253. pycharter/ui/static/static/static/documentation/__next.documentation.__PAGE__.txt +2 -2
  254. pycharter/ui/static/static/static/documentation/__next.documentation.txt +1 -1
  255. pycharter/ui/static/static/static/documentation/index.html +2 -2
  256. pycharter/ui/static/static/static/documentation/index.txt +3 -3
  257. pycharter/ui/static/static/static/index.html +1 -1
  258. pycharter/ui/static/static/static/index.txt +2 -2
  259. pycharter/ui/static/static/static/metadata/__next._full.txt +2 -2
  260. pycharter/ui/static/static/static/metadata/__next._head.txt +1 -1
  261. pycharter/ui/static/static/static/metadata/__next._index.txt +2 -2
  262. pycharter/ui/static/static/static/metadata/__next._tree.txt +2 -2
  263. pycharter/ui/static/static/static/metadata/__next.metadata.__PAGE__.txt +1 -1
  264. pycharter/ui/static/static/static/metadata/__next.metadata.txt +1 -1
  265. pycharter/ui/static/static/static/metadata/index.html +1 -1
  266. pycharter/ui/static/static/static/metadata/index.txt +2 -2
  267. pycharter/ui/static/static/static/quality/__next._full.txt +2 -2
  268. pycharter/ui/static/static/static/quality/__next._head.txt +1 -1
  269. pycharter/ui/static/static/static/quality/__next._index.txt +2 -2
  270. pycharter/ui/static/static/static/quality/__next._tree.txt +2 -2
  271. pycharter/ui/static/static/static/quality/__next.quality.__PAGE__.txt +1 -1
  272. pycharter/ui/static/static/static/quality/__next.quality.txt +1 -1
  273. pycharter/ui/static/static/static/quality/index.html +2 -2
  274. pycharter/ui/static/static/static/quality/index.txt +2 -2
  275. pycharter/ui/static/static/static/rules/__next._full.txt +2 -2
  276. pycharter/ui/static/static/static/rules/__next._head.txt +1 -1
  277. pycharter/ui/static/static/static/rules/__next._index.txt +2 -2
  278. pycharter/ui/static/static/static/rules/__next._tree.txt +2 -2
  279. pycharter/ui/static/static/static/rules/__next.rules.__PAGE__.txt +1 -1
  280. pycharter/ui/static/static/static/rules/__next.rules.txt +1 -1
  281. pycharter/ui/static/static/static/rules/index.html +1 -1
  282. pycharter/ui/static/static/static/rules/index.txt +2 -2
  283. pycharter/ui/static/static/static/schemas/__next._full.txt +2 -2
  284. pycharter/ui/static/static/static/schemas/__next._head.txt +1 -1
  285. pycharter/ui/static/static/static/schemas/__next._index.txt +2 -2
  286. pycharter/ui/static/static/static/schemas/__next._tree.txt +2 -2
  287. pycharter/ui/static/static/static/schemas/__next.schemas.__PAGE__.txt +1 -1
  288. pycharter/ui/static/static/static/schemas/__next.schemas.txt +1 -1
  289. pycharter/ui/static/static/static/schemas/index.html +1 -1
  290. pycharter/ui/static/static/static/schemas/index.txt +2 -2
  291. pycharter/ui/static/static/static/settings/__next._full.txt +2 -2
  292. pycharter/ui/static/static/static/settings/__next._head.txt +1 -1
  293. pycharter/ui/static/static/static/settings/__next._index.txt +2 -2
  294. pycharter/ui/static/static/static/settings/__next._tree.txt +2 -2
  295. pycharter/ui/static/static/static/settings/__next.settings.__PAGE__.txt +1 -1
  296. pycharter/ui/static/static/static/settings/__next.settings.txt +1 -1
  297. pycharter/ui/static/static/static/settings/index.html +1 -1
  298. pycharter/ui/static/static/static/settings/index.txt +2 -2
  299. pycharter/ui/static/static/static/static/.gitkeep +0 -0
  300. pycharter/ui/static/static/static/static/404/index.html +1 -0
  301. pycharter/ui/static/static/static/static/404.html +1 -0
  302. pycharter/ui/static/static/static/static/__next.__PAGE__.txt +10 -0
  303. pycharter/ui/static/static/static/static/__next._full.txt +30 -0
  304. pycharter/ui/static/static/static/static/__next._head.txt +7 -0
  305. pycharter/ui/static/static/static/static/__next._index.txt +9 -0
  306. pycharter/ui/static/static/static/static/__next._tree.txt +2 -0
  307. pycharter/ui/static/static/static/static/_next/static/chunks/222442f6da32302a.js +1 -0
  308. pycharter/ui/static/static/static/static/_next/static/chunks/247eb132b7f7b574.js +1 -0
  309. pycharter/ui/static/static/static/static/_next/static/chunks/297d55555b71baba.js +1 -0
  310. pycharter/ui/static/static/static/static/_next/static/chunks/414e77373f8ff61c.js +1 -0
  311. pycharter/ui/static/static/static/static/_next/static/chunks/652ad0aa26265c47.js +2 -0
  312. pycharter/ui/static/static/static/static/_next/static/chunks/9c23f44fff36548a.js +1 -0
  313. pycharter/ui/static/static/static/static/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  314. pycharter/ui/static/static/static/static/_next/static/chunks/b32a0963684b9933.js +4 -0
  315. pycharter/ui/static/static/static/static/_next/static/chunks/db913959c675cea6.js +1 -0
  316. pycharter/ui/static/static/static/static/_next/static/chunks/f2e7afeab1178138.js +1 -0
  317. pycharter/ui/static/static/static/static/_next/static/chunks/ff1a16fafef87110.js +1 -0
  318. pycharter/ui/static/static/static/static/_next/static/chunks/turbopack-ffcb7ab6794027ef.js +3 -0
  319. pycharter/ui/static/static/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_buildManifest.js +11 -0
  320. pycharter/ui/static/static/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_clientMiddlewareManifest.json +1 -0
  321. pycharter/ui/static/static/static/static/_next/static/tNTkVW6puVXC4bAm4WrHl/_ssgManifest.js +1 -0
  322. pycharter/ui/static/static/static/static/_not-found/__next._full.txt +17 -0
  323. pycharter/ui/static/static/static/static/_not-found/__next._head.txt +7 -0
  324. pycharter/ui/static/static/static/static/_not-found/__next._index.txt +9 -0
  325. pycharter/ui/static/static/static/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
  326. pycharter/ui/static/static/static/static/_not-found/__next._not-found.txt +4 -0
  327. pycharter/ui/static/static/static/static/_not-found/__next._tree.txt +2 -0
  328. pycharter/ui/static/static/static/static/_not-found/index.html +1 -0
  329. pycharter/ui/static/static/static/static/_not-found/index.txt +17 -0
  330. pycharter/ui/static/static/static/static/contracts/__next._full.txt +21 -0
  331. pycharter/ui/static/static/static/static/contracts/__next._head.txt +7 -0
  332. pycharter/ui/static/static/static/static/contracts/__next._index.txt +9 -0
  333. pycharter/ui/static/static/static/static/contracts/__next._tree.txt +2 -0
  334. pycharter/ui/static/static/static/static/contracts/__next.contracts.__PAGE__.txt +9 -0
  335. pycharter/ui/static/static/static/static/contracts/__next.contracts.txt +4 -0
  336. pycharter/ui/static/static/static/static/contracts/index.html +1 -0
  337. pycharter/ui/static/static/static/static/contracts/index.txt +21 -0
  338. pycharter/ui/static/static/static/static/documentation/__next._full.txt +21 -0
  339. pycharter/ui/static/static/static/static/documentation/__next._head.txt +7 -0
  340. pycharter/ui/static/static/static/static/documentation/__next._index.txt +9 -0
  341. pycharter/ui/static/static/static/static/documentation/__next._tree.txt +2 -0
  342. pycharter/ui/static/static/static/static/documentation/__next.documentation.__PAGE__.txt +9 -0
  343. pycharter/ui/static/static/static/static/documentation/__next.documentation.txt +4 -0
  344. pycharter/ui/static/static/static/static/documentation/index.html +93 -0
  345. pycharter/ui/static/static/static/static/documentation/index.txt +21 -0
  346. pycharter/ui/static/static/static/static/index.html +1 -0
  347. pycharter/ui/static/static/static/static/index.txt +30 -0
  348. pycharter/ui/static/static/static/static/metadata/__next._full.txt +21 -0
  349. pycharter/ui/static/static/static/static/metadata/__next._head.txt +7 -0
  350. pycharter/ui/static/static/static/static/metadata/__next._index.txt +9 -0
  351. pycharter/ui/static/static/static/static/metadata/__next._tree.txt +2 -0
  352. pycharter/ui/static/static/static/static/metadata/__next.metadata.__PAGE__.txt +9 -0
  353. pycharter/ui/static/static/static/static/metadata/__next.metadata.txt +4 -0
  354. pycharter/ui/static/static/static/static/metadata/index.html +1 -0
  355. pycharter/ui/static/static/static/static/metadata/index.txt +21 -0
  356. pycharter/ui/static/static/static/static/quality/__next._full.txt +21 -0
  357. pycharter/ui/static/static/static/static/quality/__next._head.txt +7 -0
  358. pycharter/ui/static/static/static/static/quality/__next._index.txt +9 -0
  359. pycharter/ui/static/static/static/static/quality/__next._tree.txt +2 -0
  360. pycharter/ui/static/static/static/static/quality/__next.quality.__PAGE__.txt +9 -0
  361. pycharter/ui/static/static/static/static/quality/__next.quality.txt +4 -0
  362. pycharter/ui/static/static/static/static/quality/index.html +2 -0
  363. pycharter/ui/static/static/static/static/quality/index.txt +21 -0
  364. pycharter/ui/static/static/static/static/rules/__next._full.txt +21 -0
  365. pycharter/ui/static/static/static/static/rules/__next._head.txt +7 -0
  366. pycharter/ui/static/static/static/static/rules/__next._index.txt +9 -0
  367. pycharter/ui/static/static/static/static/rules/__next._tree.txt +2 -0
  368. pycharter/ui/static/static/static/static/rules/__next.rules.__PAGE__.txt +9 -0
  369. pycharter/ui/static/static/static/static/rules/__next.rules.txt +4 -0
  370. pycharter/ui/static/static/static/static/rules/index.html +1 -0
  371. pycharter/ui/static/static/static/static/rules/index.txt +21 -0
  372. pycharter/ui/static/static/static/static/schemas/__next._full.txt +21 -0
  373. pycharter/ui/static/static/static/static/schemas/__next._head.txt +7 -0
  374. pycharter/ui/static/static/static/static/schemas/__next._index.txt +9 -0
  375. pycharter/ui/static/static/static/static/schemas/__next._tree.txt +2 -0
  376. pycharter/ui/static/static/static/static/schemas/__next.schemas.__PAGE__.txt +9 -0
  377. pycharter/ui/static/static/static/static/schemas/__next.schemas.txt +4 -0
  378. pycharter/ui/static/static/static/static/schemas/index.html +1 -0
  379. pycharter/ui/static/static/static/static/schemas/index.txt +21 -0
  380. pycharter/ui/static/static/static/static/settings/__next._full.txt +21 -0
  381. pycharter/ui/static/static/static/static/settings/__next._head.txt +7 -0
  382. pycharter/ui/static/static/static/static/settings/__next._index.txt +9 -0
  383. pycharter/ui/static/static/static/static/settings/__next._tree.txt +2 -0
  384. pycharter/ui/static/static/static/static/settings/__next.settings.__PAGE__.txt +9 -0
  385. pycharter/ui/static/static/static/static/settings/__next.settings.txt +4 -0
  386. pycharter/ui/static/static/static/static/settings/index.html +1 -0
  387. pycharter/ui/static/static/static/static/settings/index.txt +21 -0
  388. pycharter/ui/static/static/static/static/validation/__next._full.txt +21 -0
  389. pycharter/ui/static/static/static/static/validation/__next._head.txt +7 -0
  390. pycharter/ui/static/static/static/static/validation/__next._index.txt +9 -0
  391. pycharter/ui/static/static/static/static/validation/__next._tree.txt +2 -0
  392. pycharter/ui/static/static/static/static/validation/__next.validation.__PAGE__.txt +9 -0
  393. pycharter/ui/static/static/static/static/validation/__next.validation.txt +4 -0
  394. pycharter/ui/static/static/static/static/validation/index.html +1 -0
  395. pycharter/ui/static/static/static/static/validation/index.txt +21 -0
  396. pycharter/ui/static/static/static/validation/__next._full.txt +2 -2
  397. pycharter/ui/static/static/static/validation/__next._head.txt +1 -1
  398. pycharter/ui/static/static/static/validation/__next._index.txt +2 -2
  399. pycharter/ui/static/static/static/validation/__next._tree.txt +2 -2
  400. pycharter/ui/static/static/static/validation/__next.validation.__PAGE__.txt +1 -1
  401. pycharter/ui/static/static/static/validation/__next.validation.txt +1 -1
  402. pycharter/ui/static/static/static/validation/index.html +1 -1
  403. pycharter/ui/static/static/static/validation/index.txt +2 -2
  404. pycharter/ui/static/static/validation/__next._full.txt +2 -2
  405. pycharter/ui/static/static/validation/__next._head.txt +1 -1
  406. pycharter/ui/static/static/validation/__next._index.txt +1 -1
  407. pycharter/ui/static/static/validation/__next._tree.txt +1 -1
  408. pycharter/ui/static/static/validation/__next.validation.__PAGE__.txt +2 -2
  409. pycharter/ui/static/static/validation/__next.validation.txt +1 -1
  410. pycharter/ui/static/static/validation/index.html +1 -1
  411. pycharter/ui/static/static/validation/index.txt +2 -2
  412. pycharter/ui/static/validation/__next._full.txt +7 -7
  413. pycharter/ui/static/validation/__next._head.txt +1 -1
  414. pycharter/ui/static/validation/__next._index.txt +6 -6
  415. pycharter/ui/static/validation/__next._tree.txt +2 -2
  416. pycharter/ui/static/validation/__next.validation.__PAGE__.txt +2 -2
  417. pycharter/ui/static/validation/__next.validation.txt +1 -1
  418. pycharter/ui/static/validation/index.html +1 -1
  419. pycharter/ui/static/validation/index.txt +7 -7
  420. {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/METADATA +57 -26
  421. pycharter-0.0.26.dist-info/RECORD +702 -0
  422. pycharter/etl_generator/config_loader.py +0 -394
  423. pycharter/etl_generator/loaders/cloud.py +0 -87
  424. pycharter/etl_generator/loaders/file_loader.py +0 -130
  425. pycharter/etl_generator/schemas/extract.json +0 -234
  426. pycharter/etl_generator/schemas/load.json +0 -202
  427. pycharter/etl_generator/schemas/pipeline.json +0 -94
  428. pycharter/etl_generator/schemas/transform.json +0 -171
  429. pycharter-0.0.25.dist-info/RECORD +0 -572
  430. /pycharter/ui/static/_next/static/{2gKjNv6YvE6BcIdFthBLs → YCnlK66gA7FV5vvcixspB}/_buildManifest.js +0 -0
  431. /pycharter/ui/static/_next/static/{2gKjNv6YvE6BcIdFthBLs → YCnlK66gA7FV5vvcixspB}/_clientMiddlewareManifest.json +0 -0
  432. /pycharter/ui/static/_next/static/{2gKjNv6YvE6BcIdFthBLs → YCnlK66gA7FV5vvcixspB}/_ssgManifest.js +0 -0
  433. /pycharter/ui/static/static/_next/static/{0rYA78L88aUyD2Uh38hhX → 2gKjNv6YvE6BcIdFthBLs}/_buildManifest.js +0 -0
  434. /pycharter/ui/static/static/_next/static/{0rYA78L88aUyD2Uh38hhX → 2gKjNv6YvE6BcIdFthBLs}/_clientMiddlewareManifest.json +0 -0
  435. /pycharter/ui/static/static/_next/static/{0rYA78L88aUyD2Uh38hhX → 2gKjNv6YvE6BcIdFthBLs}/_ssgManifest.js +0 -0
  436. /pycharter/ui/static/{_next → static/_next}/static/chunks/26dfc590f7714c03.js +0 -0
  437. /pycharter/ui/static/{_next → static/_next}/static/chunks/34d289e6db2ef551.js +0 -0
  438. /pycharter/ui/static/{_next → static/_next}/static/chunks/99508d9d5869cc27.js +0 -0
  439. /pycharter/ui/static/{_next → static/_next}/static/chunks/b313c35a6ba76574.js +0 -0
  440. /pycharter/ui/static/static/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_buildManifest.js +0 -0
  441. /pycharter/ui/static/static/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_clientMiddlewareManifest.json +0 -0
  442. /pycharter/ui/static/static/static/_next/static/{tNTkVW6puVXC4bAm4WrHl → 0rYA78L88aUyD2Uh38hhX}/_ssgManifest.js +0 -0
  443. /pycharter/ui/static/{_next → static/static/_next}/static/chunks/13d4a0fbd74c1ee4.js +0 -0
  444. /pycharter/ui/static/{_next → static/static/_next}/static/chunks/2edb43b48432ac04.js +0 -0
  445. /pycharter/ui/static/static/{_next → static/_next}/static/chunks/c4fa4f4114b7c352.js +0 -0
  446. /pycharter/ui/static/{_next → static/static/_next}/static/chunks/d2363397e1b2bcab.css +0 -0
  447. /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/2ab439ce003cd691.js +0 -0
  448. /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/49ca65abd26ae49e.js +0 -0
  449. /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/4e310fe5005770a3.css +0 -0
  450. /pycharter/ui/static/static/{_next → static/static/_next}/static/chunks/5e04d10c4a7b58a3.js +0 -0
  451. /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/5fc14c00a2779dc5.js +0 -0
  452. /pycharter/ui/static/static/{_next → static/static/_next}/static/chunks/75d88a058d8ffaa6.js +0 -0
  453. /pycharter/ui/static/static/{_next → static/static/_next}/static/chunks/8c89634cf6bad76f.js +0 -0
  454. /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/9667e7a3d359eb39.js +0 -0
  455. /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/b584574fdc8ab13e.js +0 -0
  456. /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/c69f6cba366bd988.js +0 -0
  457. /pycharter/ui/static/static/static/{_next → static/_next}/static/chunks/d5989c94d3614b3a.js +0 -0
  458. /pycharter/ui/static/{_next → static/static/static/_next}/static/chunks/f061a4be97bfc3b3.js +0 -0
  459. {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/WHEEL +0 -0
  460. {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/entry_points.txt +0 -0
  461. {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/licenses/LICENSE +0 -0
  462. {pycharter-0.0.25.dist-info → pycharter-0.0.26.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,551 @@
1
+ """
2
+ ETL Validation Module.
3
+
4
+ Provides validation for ETL pipelines:
5
+ - ETLValidator: Validates data against schemas/contracts with DLQ support
6
+ - resolve_contract: Resolves contract references (file or database)
7
+ - resolve_schema: Resolves schema file paths
8
+ """
9
+
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Tuple, Union
13
+
14
+ import yaml
15
+
16
+ from pycharter.etl_generator.config_models import (
17
+ ContractRef,
18
+ DLQConfig,
19
+ DLQBackend,
20
+ ExtractValidationConfig,
21
+ LoadValidationConfig,
22
+ OnErrorAction,
23
+ )
24
+ from pycharter.etl_generator.dlq import DeadLetterQueue, DLQReason
25
+ from pycharter.runtime_validator import Validator
26
+ from pycharter.metadata_store import MetadataStoreClient
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class ETLValidationError(Exception):
32
+ """Raised when validation fails with on_error=fail."""
33
+
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ invalid_count: int = 0,
38
+ errors: Optional[List[str]] = None,
39
+ ):
40
+ super().__init__(message)
41
+ self.invalid_count = invalid_count
42
+ self.errors = errors or []
43
+
44
+
45
+ class ETLValidator:
46
+ """
47
+ Validates data in ETL pipelines with configurable error handling.
48
+
49
+ Supports:
50
+ - Source validation (after extract) against a schema file
51
+ - Target validation (before load) against a data contract
52
+ - Error handling modes: fail, warn, skip, quarantine
53
+ - DLQ integration for quarantining invalid records
54
+
55
+ Example:
56
+ >>> validator = ETLValidator(
57
+ ... schema_or_contract={"type": "object", "properties": {...}},
58
+ ... on_error=OnErrorAction.QUARANTINE,
59
+ ... dlq=dlq_instance,
60
+ ... pipeline_name="orders_pipeline",
61
+ ... stage="extract",
62
+ ... )
63
+ >>> valid, invalid, error_count = await validator.validate(data)
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ schema_or_contract: Dict[str, Any],
69
+ on_error: OnErrorAction = OnErrorAction.FAIL,
70
+ dlq: Optional[DeadLetterQueue] = None,
71
+ pipeline_name: str = "",
72
+ stage: str = "",
73
+ ):
74
+ """
75
+ Initialize the ETL validator.
76
+
77
+ Args:
78
+ schema_or_contract: JSON Schema dict or complete contract dict
79
+ on_error: Action on validation error (fail, warn, skip, quarantine)
80
+ dlq: Optional DeadLetterQueue instance for quarantine mode
81
+ pipeline_name: Name of the pipeline (for DLQ metadata)
82
+ stage: ETL stage (extract or load) for DLQ metadata
83
+ """
84
+ self.on_error = on_error
85
+ self.dlq = dlq
86
+ self.pipeline_name = pipeline_name
87
+ self.stage = stage
88
+
89
+ # Initialize the underlying Validator
90
+ # If it's a full contract (has 'schema' key), load it as contract_dict
91
+ # Otherwise treat it as a schema directly
92
+ if "schema" in schema_or_contract and isinstance(
93
+ schema_or_contract["schema"], dict
94
+ ):
95
+ schema = schema_or_contract["schema"]
96
+ # Ensure schema has version (required by Validator)
97
+ if "version" not in schema:
98
+ schema["version"] = "1.0.0"
99
+ self._validator = Validator(contract_dict=schema_or_contract)
100
+ else:
101
+ # It's a schema, wrap it in contract format
102
+ # Ensure schema has version (required by Validator)
103
+ if "version" not in schema_or_contract:
104
+ schema_or_contract = {**schema_or_contract, "version": "1.0.0"}
105
+ self._validator = Validator(contract_dict={"schema": schema_or_contract})
106
+
107
+ async def validate(
108
+ self,
109
+ data: List[Dict[str, Any]],
110
+ run_id: str = "",
111
+ ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], int]:
112
+ """
113
+ Validate a batch of data records.
114
+
115
+ Args:
116
+ data: List of data records to validate
117
+ run_id: Optional run identifier for DLQ metadata
118
+
119
+ Returns:
120
+ Tuple of (valid_records, quarantined_records, error_count)
121
+
122
+ Raises:
123
+ ETLValidationError: If on_error=fail and validation fails
124
+ """
125
+ if not data:
126
+ return [], [], 0
127
+
128
+ valid_records: List[Dict[str, Any]] = []
129
+ invalid_records: List[Dict[str, Any]] = []
130
+ all_errors: List[str] = []
131
+
132
+ # Validate each record
133
+ results = self._validator.validate_batch(data, strict=False)
134
+
135
+ for record, result in zip(data, results):
136
+ if result.is_valid:
137
+ # Return the original dict, not the Pydantic model
138
+ valid_records.append(record)
139
+ else:
140
+ invalid_records.append({
141
+ "record": record,
142
+ "errors": result.errors,
143
+ })
144
+ all_errors.extend(result.errors)
145
+
146
+ error_count = len(invalid_records)
147
+
148
+ # Handle based on on_error setting
149
+ if invalid_records:
150
+ if self.on_error == OnErrorAction.FAIL:
151
+ raise ETLValidationError(
152
+ f"Validation failed: {error_count} record(s) invalid",
153
+ invalid_count=error_count,
154
+ errors=all_errors[:10], # Limit errors in exception
155
+ )
156
+
157
+ elif self.on_error == OnErrorAction.WARN:
158
+ logger.warning(
159
+ f"[{self.pipeline_name}] Validation warning: "
160
+ f"{error_count} record(s) invalid in {self.stage} stage"
161
+ )
162
+ # Return all records (including invalid) for warn mode
163
+ return data, [], error_count
164
+
165
+ elif self.on_error == OnErrorAction.SKIP:
166
+ logger.info(
167
+ f"[{self.pipeline_name}] Skipping {error_count} invalid record(s) "
168
+ f"in {self.stage} stage"
169
+ )
170
+ # Return only valid records, no quarantine
171
+ return valid_records, [], error_count
172
+
173
+ elif self.on_error == OnErrorAction.QUARANTINE:
174
+ logger.info(
175
+ f"[{self.pipeline_name}] Quarantining {error_count} invalid record(s) "
176
+ f"in {self.stage} stage"
177
+ )
178
+ # Send to DLQ if configured
179
+ if self.dlq:
180
+ await self._send_to_dlq(invalid_records, run_id)
181
+ # Return valid records, quarantined records
182
+ return (
183
+ valid_records,
184
+ [item["record"] for item in invalid_records],
185
+ error_count,
186
+ )
187
+
188
+ return valid_records, [], 0
189
+
190
+ async def _send_to_dlq(
191
+ self,
192
+ invalid_records: List[Dict[str, Any]],
193
+ run_id: str = "",
194
+ ) -> None:
195
+ """Send invalid records to the Dead Letter Queue."""
196
+ reason = (
197
+ DLQReason.SCHEMA_MISMATCH
198
+ if self.stage == "extract"
199
+ else DLQReason.VALIDATION_ERROR
200
+ )
201
+
202
+ for item in invalid_records:
203
+ await self.dlq.add_record(
204
+ pipeline_name=self.pipeline_name,
205
+ record_data=item["record"],
206
+ reason=reason,
207
+ error_message="; ".join(item["errors"][:5]), # Limit error message length
208
+ error_type="ValidationError",
209
+ stage=self.stage,
210
+ metadata={
211
+ "run_id": run_id,
212
+ "error_count": len(item["errors"]),
213
+ "validation_errors": item["errors"][:10], # Include detailed errors
214
+ },
215
+ )
216
+
217
+
218
+ def resolve_schema(
219
+ schema_ref: str,
220
+ base_dir: Optional[Path] = None,
221
+ ) -> Dict[str, Any]:
222
+ """
223
+ Resolve a schema file path to a schema dict.
224
+
225
+ Args:
226
+ schema_ref: Path to schema file (absolute or relative)
227
+ base_dir: Base directory for relative paths
228
+
229
+ Returns:
230
+ Schema dict
231
+
232
+ Raises:
233
+ FileNotFoundError: If schema file not found
234
+ ValueError: If schema file is invalid
235
+ """
236
+ schema_path = Path(schema_ref)
237
+
238
+ # Resolve relative paths
239
+ if not schema_path.is_absolute() and base_dir:
240
+ schema_path = base_dir / schema_path
241
+
242
+ if not schema_path.exists():
243
+ raise FileNotFoundError(f"Schema file not found: {schema_path}")
244
+
245
+ with open(schema_path) as f:
246
+ schema = yaml.safe_load(f)
247
+
248
+ if not isinstance(schema, dict):
249
+ raise ValueError(f"Invalid schema file: {schema_path}")
250
+
251
+ return schema
252
+
253
+
254
+ def resolve_contract(
255
+ contract_ref: Union[str, ContractRef, Dict[str, Any]],
256
+ metadata_store: Optional[MetadataStoreClient] = None,
257
+ base_dir: Optional[Path] = None,
258
+ ) -> Dict[str, Any]:
259
+ """
260
+ Resolve a contract reference to a complete contract dict.
261
+
262
+ Supports:
263
+ - String path: "contracts/orders/schema.yaml" (file path)
264
+ - ContractRef object: {"type": "database", "name": "orders", "version": "2.0"}
265
+ - Dict with 'name' key: {"name": "orders"} (uses metadata_store)
266
+ - Dict with 'type': file' and 'path' key
267
+
268
+ Args:
269
+ contract_ref: Contract reference (path, ContractRef, or dict)
270
+ metadata_store: Optional MetadataStoreClient for database contracts
271
+ base_dir: Base directory for relative file paths
272
+
273
+ Returns:
274
+ Complete contract dict with schema (and optionally coercion/validation rules)
275
+
276
+ Raises:
277
+ FileNotFoundError: If file-based contract not found
278
+ ValueError: If contract reference is invalid or store not provided
279
+ """
280
+ # Case 1: String path (file-based)
281
+ if isinstance(contract_ref, str):
282
+ return _load_contract_from_file(contract_ref, base_dir)
283
+
284
+ # Case 2: ContractRef Pydantic model
285
+ if isinstance(contract_ref, ContractRef):
286
+ if not metadata_store:
287
+ raise ValueError(
288
+ "metadata_store required for database contract reference"
289
+ )
290
+ return _load_contract_from_store(
291
+ metadata_store, contract_ref.name, contract_ref.version
292
+ )
293
+
294
+ # Case 3: Dict
295
+ if isinstance(contract_ref, dict):
296
+ ref_type = contract_ref.get("type", "file")
297
+
298
+ if ref_type == "file":
299
+ path = contract_ref.get("path")
300
+ if not path:
301
+ raise ValueError("Contract reference missing 'path' field")
302
+ return _load_contract_from_file(path, base_dir)
303
+
304
+ elif ref_type == "database":
305
+ name = contract_ref.get("name")
306
+ if not name:
307
+ raise ValueError("Contract reference missing 'name' field")
308
+ if not metadata_store:
309
+ raise ValueError(
310
+ "metadata_store required for database contract reference"
311
+ )
312
+ return _load_contract_from_store(
313
+ metadata_store, name, contract_ref.get("version")
314
+ )
315
+
316
+ # Case 4: Dict with just 'name' (shorthand for database)
317
+ elif "name" in contract_ref:
318
+ if not metadata_store:
319
+ raise ValueError(
320
+ "metadata_store required for contract name reference"
321
+ )
322
+ return _load_contract_from_store(
323
+ metadata_store,
324
+ contract_ref["name"],
325
+ contract_ref.get("version"),
326
+ )
327
+
328
+ else:
329
+ raise ValueError(f"Unknown contract reference type: {ref_type}")
330
+
331
+ raise ValueError(f"Invalid contract reference: {contract_ref}")
332
+
333
+
334
+ def _load_contract_from_file(
335
+ path: str,
336
+ base_dir: Optional[Path] = None,
337
+ ) -> Dict[str, Any]:
338
+ """Load contract from file path."""
339
+ contract_path = Path(path)
340
+
341
+ # Resolve relative paths
342
+ if not contract_path.is_absolute() and base_dir:
343
+ contract_path = base_dir / contract_path
344
+
345
+ # Check if it's a directory (contract directory) or file
346
+ if contract_path.is_dir():
347
+ return _load_contract_from_directory(contract_path)
348
+ elif contract_path.exists():
349
+ return _load_contract_from_single_file(contract_path)
350
+ else:
351
+ # Try adding common extensions
352
+ for ext in (".yaml", ".yml", ".json"):
353
+ if contract_path.with_suffix(ext).exists():
354
+ return _load_contract_from_single_file(contract_path.with_suffix(ext))
355
+
356
+ raise FileNotFoundError(f"Contract not found: {contract_path}")
357
+
358
+
359
+ def _load_contract_from_directory(directory: Path) -> Dict[str, Any]:
360
+ """Load contract from directory with separate files."""
361
+ contract: Dict[str, Any] = {}
362
+
363
+ # Load schema (required)
364
+ schema_path = directory / "schema.yaml"
365
+ if not schema_path.exists():
366
+ schema_path = directory / "schema.yml"
367
+ if not schema_path.exists():
368
+ raise FileNotFoundError(f"Schema not found in contract directory: {directory}")
369
+
370
+ with open(schema_path) as f:
371
+ contract["schema"] = yaml.safe_load(f)
372
+
373
+ # Load coercion rules (optional)
374
+ for name in ("coercion_rules.yaml", "coercion_rules.yml", "coercion.yaml"):
375
+ coercion_path = directory / name
376
+ if coercion_path.exists():
377
+ with open(coercion_path) as f:
378
+ contract["coercion_rules"] = yaml.safe_load(f)
379
+ break
380
+
381
+ # Load validation rules (optional)
382
+ for name in ("validation_rules.yaml", "validation_rules.yml", "validation.yaml"):
383
+ validation_path = directory / name
384
+ if validation_path.exists():
385
+ with open(validation_path) as f:
386
+ contract["validation_rules"] = yaml.safe_load(f)
387
+ break
388
+
389
+ return contract
390
+
391
+
392
+ def _load_contract_from_single_file(path: Path) -> Dict[str, Any]:
393
+ """Load contract from single file."""
394
+ with open(path) as f:
395
+ content = yaml.safe_load(f)
396
+
397
+ if not isinstance(content, dict):
398
+ raise ValueError(f"Invalid contract file: {path}")
399
+
400
+ # If it has a 'schema' key, it's a complete contract
401
+ if "schema" in content:
402
+ return content
403
+
404
+ # Otherwise treat the whole file as a schema
405
+ return {"schema": content}
406
+
407
+
408
+ def _load_contract_from_store(
409
+ store: MetadataStoreClient,
410
+ name: str,
411
+ version: Optional[str] = None,
412
+ ) -> Dict[str, Any]:
413
+ """Load contract from metadata store.
414
+
415
+ Returns raw schema + separate rules. The Validator handles merging internally.
416
+ """
417
+ # Get raw schema (not merged)
418
+ schema = store.get_schema(name, version)
419
+
420
+ if not schema:
421
+ raise ValueError(f"Contract not found in store: {name} (version: {version})")
422
+
423
+ # Get coercion and validation rules separately
424
+ coercion_rules = store.get_coercion_rules(name, version)
425
+ validation_rules = store.get_validation_rules(name, version)
426
+
427
+ return {
428
+ "schema": schema,
429
+ "coercion_rules": coercion_rules or {},
430
+ "validation_rules": validation_rules or {},
431
+ }
432
+
433
+
434
+ def create_dlq(
435
+ dlq_config: Optional[Union[bool, DLQConfig, Dict[str, Any]]],
436
+ default_config: Optional[DLQConfig] = None,
437
+ db_session: Optional[Any] = None,
438
+ ) -> Optional[DeadLetterQueue]:
439
+ """
440
+ Create a DeadLetterQueue from configuration.
441
+
442
+ Args:
443
+ dlq_config: DLQ configuration (bool, DLQConfig, or dict)
444
+ default_config: Default DLQ config from settings
445
+ db_session: Database session for database backend
446
+
447
+ Returns:
448
+ DeadLetterQueue instance or None if disabled
449
+ """
450
+ # Handle bool shorthand
451
+ if dlq_config is True:
452
+ # Use default config if available
453
+ if default_config:
454
+ dlq_config = default_config
455
+ else:
456
+ # Create minimal file-based DLQ
457
+ dlq_config = DLQConfig(enabled=True, backend=DLQBackend.FILE, path="./dlq")
458
+ elif dlq_config is False or dlq_config is None:
459
+ return None
460
+
461
+ # Convert dict to DLQConfig if needed
462
+ if isinstance(dlq_config, dict):
463
+ dlq_config = DLQConfig(**dlq_config)
464
+
465
+ if not dlq_config.enabled:
466
+ return None
467
+
468
+ # Create DLQ based on backend
469
+ return DeadLetterQueue(
470
+ db_session=db_session if dlq_config.backend == DLQBackend.DATABASE else None,
471
+ storage_backend=dlq_config.backend.value,
472
+ storage_path=dlq_config.path,
473
+ enabled=dlq_config.enabled,
474
+ schema_name=dlq_config.schema_name,
475
+ )
476
+
477
+
478
+ def create_etl_validator(
479
+ validation_config: Union[ExtractValidationConfig, LoadValidationConfig, Dict[str, Any]],
480
+ pipeline_name: str,
481
+ stage: str,
482
+ metadata_store: Optional[MetadataStoreClient] = None,
483
+ default_contract: Optional[str] = None,
484
+ default_dlq_config: Optional[DLQConfig] = None,
485
+ base_dir: Optional[Path] = None,
486
+ db_session: Optional[Any] = None,
487
+ ) -> Optional[ETLValidator]:
488
+ """
489
+ Create an ETLValidator from validation configuration.
490
+
491
+ Args:
492
+ validation_config: Validation config (ExtractValidationConfig, LoadValidationConfig, or dict)
493
+ pipeline_name: Pipeline name for DLQ metadata
494
+ stage: ETL stage ('extract' or 'load')
495
+ metadata_store: Optional metadata store for database contracts
496
+ default_contract: Default contract name from settings
497
+ default_dlq_config: Default DLQ config from settings
498
+ base_dir: Base directory for relative file paths
499
+ db_session: Database session for DLQ database backend
500
+
501
+ Returns:
502
+ ETLValidator instance or None if validation not configured
503
+ """
504
+ if validation_config is None:
505
+ return None
506
+
507
+ # Convert dict to appropriate config type
508
+ if isinstance(validation_config, dict):
509
+ if stage == "extract":
510
+ validation_config = ExtractValidationConfig(**validation_config)
511
+ else:
512
+ validation_config = LoadValidationConfig(**validation_config)
513
+
514
+ # Resolve schema/contract
515
+ schema_or_contract: Optional[Dict[str, Any]] = None
516
+
517
+ if isinstance(validation_config, ExtractValidationConfig):
518
+ # Extract validation uses schema file
519
+ if validation_config.schema_path:
520
+ schema_or_contract = resolve_schema(validation_config.schema_path, base_dir)
521
+ else:
522
+ logger.warning(f"Extract validation config missing 'schema' field")
523
+ return None
524
+
525
+ elif isinstance(validation_config, LoadValidationConfig):
526
+ # Load validation uses contract
527
+ if validation_config.contract:
528
+ schema_or_contract = resolve_contract(
529
+ validation_config.contract, metadata_store, base_dir
530
+ )
531
+ elif validation_config.use_contract and default_contract:
532
+ schema_or_contract = resolve_contract(
533
+ {"name": default_contract}, metadata_store, base_dir
534
+ )
535
+ else:
536
+ logger.warning(f"Load validation config missing 'contract' field")
537
+ return None
538
+
539
+ if schema_or_contract is None:
540
+ return None
541
+
542
+ # Create DLQ if configured
543
+ dlq = create_dlq(validation_config.dlq, default_dlq_config, db_session)
544
+
545
+ return ETLValidator(
546
+ schema_or_contract=schema_or_contract,
547
+ on_error=validation_config.on_error,
548
+ dlq=dlq,
549
+ pipeline_name=pipeline_name,
550
+ stage=stage,
551
+ )
@@ -62,6 +62,10 @@ from pycharter.runtime_validator.validator import (
62
62
  Validator,
63
63
  create_validator,
64
64
  )
65
+ from pycharter.runtime_validator.utils import (
66
+ merge_rules_into_schema,
67
+ get_merged_schema_from_contract,
68
+ )
65
69
 
66
70
  __all__ = [
67
71
  # PRIMARY INTERFACE: Validator and Builder
@@ -85,4 +89,7 @@ __all__ = [
85
89
  "validate_input",
86
90
  "validate_output",
87
91
  "validate_with_contract_decorator",
92
+ # Schema utilities
93
+ "merge_rules_into_schema",
94
+ "get_merged_schema_from_contract",
88
95
  ]
@@ -69,3 +69,36 @@ def merge_rules_into_schema(
69
69
 
70
70
  return complete_schema
71
71
 
72
+
73
+ def get_merged_schema_from_contract(contract: Dict[str, Any]) -> Dict[str, Any]:
74
+ """
75
+ Get merged schema from a contract dictionary.
76
+
77
+ This is a convenience function for cases where you need the merged schema
78
+ (e.g., for display, documentation generation, or legacy code).
79
+
80
+ For validation, prefer using the Validator class which handles merging
81
+ internally.
82
+
83
+ Args:
84
+ contract: Contract dictionary with 'schema', 'coercion_rules', 'validation_rules' keys
85
+
86
+ Returns:
87
+ Schema dictionary with coercion and validation rules merged into properties
88
+
89
+ Example:
90
+ >>> contract = build_contract(artifacts)
91
+ >>> merged = get_merged_schema_from_contract(contract)
92
+ >>> merged["properties"]["age"]["coercion"] # Rules are now in schema
93
+ 'coerce_to_integer'
94
+
95
+ Note:
96
+ The returned schema is a deep copy - modifications won't affect the
97
+ original contract.
98
+ """
99
+ schema = contract.get("schema", {})
100
+ coercion_rules = contract.get("coercion_rules", {})
101
+ validation_rules = contract.get("validation_rules", {})
102
+
103
+ return merge_rules_into_schema(schema, coercion_rules, validation_rules)
104
+
@@ -279,8 +279,7 @@ class Validator:
279
279
  """
280
280
  Load contract from metadata store.
281
281
 
282
- The complete schema from the store already has rules merged,
283
- so we skip the merging step.
282
+ Fetches raw schema and rules separately, then merges internally.
284
283
 
285
284
  Args:
286
285
  store: MetadataStoreClient instance
@@ -290,17 +289,21 @@ class Validator:
290
289
  Raises:
291
290
  ValueError: If schema not found in store
292
291
  """
293
- complete_schema = store.get_complete_schema(schema_id, version)
294
- if not complete_schema:
292
+ # Fetch raw schema (not merged)
293
+ raw_schema = store.get_schema(schema_id, version)
294
+ if not raw_schema:
295
295
  raise ValueError(f"Schema '{schema_id}' not found in store" +
296
296
  (f" (version: {version})" if version else ""))
297
297
 
298
- # The complete_schema already has rules merged by get_complete_schema()
299
- # Store it directly - no need to merge again
300
- self.schema = complete_schema
301
- self.coercion_rules = {} # Already merged into schema
302
- self.validation_rules = {} # Already merged into schema
303
- self._schema_from_store = True # Flag to skip merging
298
+ # Fetch rules separately
299
+ coercion_rules = store.get_coercion_rules(schema_id, version)
300
+ validation_rules = store.get_validation_rules(schema_id, version)
301
+
302
+ # Store raw components - merge will happen in _merge_rules_into_schema()
303
+ self.schema = raw_schema
304
+ self.coercion_rules = self._extract_rules(coercion_rules) if coercion_rules else {}
305
+ self.validation_rules = self._extract_rules(validation_rules) if validation_rules else {}
306
+ self._schema_from_store = False # Let internal merge happen
304
307
 
305
308
  def _load_from_metadata(self, metadata: ContractMetadata) -> None:
306
309
  """