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
pycharter/__init__.py CHANGED
@@ -166,6 +166,9 @@ from pycharter.runtime_validator import (
166
166
  validate_input,
167
167
  validate_output,
168
168
  validate_with_contract_decorator,
169
+ # Schema utilities
170
+ merge_rules_into_schema,
171
+ get_merged_schema_from_contract,
169
172
  )
170
173
 
171
174
  # ============================================================================
@@ -345,6 +348,9 @@ __all__ = [
345
348
  "validate_input",
346
349
  "validate_output",
347
350
  "validate_with_contract_decorator",
351
+ # Schema utilities
352
+ "merge_rules_into_schema",
353
+ "get_merged_schema_from_contract",
348
354
  # Quality
349
355
  "QualityCheck",
350
356
  "QualityCheckOptions",
pycharter/api/README.md CHANGED
@@ -183,7 +183,7 @@ List all schemas stored in the metadata store.
183
183
  Get a schema by ID (optional version parameter).
184
184
 
185
185
  #### `GET /api/v1/metadata/schemas/{schema_id}/complete`
186
- Get complete schema with coercion and validation rules merged.
186
+ Get complete schema with coercion and validation rules merged (for display/docs). For validation, use the Validator class, which merges rules internally.
187
187
 
188
188
  #### `POST /api/v1/metadata/schemas`
189
189
  Store a schema in the metadata store.
@@ -0,0 +1,158 @@
1
+ """
2
+ Authentication dependency for API routes.
3
+
4
+ Supports:
5
+ - Local/dev: initial credentials from env or pycharter.cfg; JWT issued and verified in-process.
6
+ - Optional auth service: token introspect via HTTP (when auth_service_url + introspect_path set).
7
+ """
8
+
9
+ import hmac
10
+ import time
11
+ from typing import Any, Optional
12
+
13
+ import jwt
14
+ from fastapi import Depends, HTTPException, status
15
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
16
+
17
+ from pycharter.config import (
18
+ get_auth_initial_credentials,
19
+ get_auth_jwt_secret,
20
+ get_auth_service_introspect_path,
21
+ get_auth_service_url,
22
+ is_auth_disabled,
23
+ )
24
+
25
+ # HTTPBearer returns the token from Authorization: Bearer <token>
26
+ security = HTTPBearer(auto_error=False)
27
+
28
+ # Default JWT expiry (seconds)
29
+ ACCESS_TOKEN_EXPIRE_SECONDS = 3600 # 1 hour
30
+
31
+
32
+ def _constant_time_compare(a: str, b: str) -> bool:
33
+ """Constant-time string comparison to avoid timing attacks."""
34
+ return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
35
+
36
+
37
+ def verify_initial_credentials(username: str, password: str) -> bool:
38
+ """Verify username/password against initial credentials (env or cfg). Returns True if valid."""
39
+ creds = get_auth_initial_credentials()
40
+ if not creds:
41
+ return False
42
+ u, p = creds
43
+ return _constant_time_compare(username, u) and _constant_time_compare(password, p)
44
+
45
+
46
+ def create_access_token(username: str, expires_delta_seconds: int = ACCESS_TOKEN_EXPIRE_SECONDS) -> str:
47
+ """Create a JWT access token for the given username."""
48
+ secret = get_auth_jwt_secret()
49
+ if not secret:
50
+ raise ValueError("JWT secret not configured (set PYCHARTER_AUTH_JWT_SECRET or auth.jwt_secret)")
51
+ payload = {
52
+ "sub": username,
53
+ "exp": int(time.time()) + expires_delta_seconds,
54
+ "iat": int(time.time()),
55
+ }
56
+ return jwt.encode(payload, secret, algorithm="HS256")
57
+
58
+
59
+ def verify_jwt(token: str) -> Optional[dict[str, Any]]:
60
+ """Verify JWT and return payload (with 'sub' = username) or None if invalid."""
61
+ secret = get_auth_jwt_secret()
62
+ if not secret:
63
+ return None
64
+ try:
65
+ payload = jwt.decode(token, secret, algorithms=["HS256"])
66
+ return payload
67
+ except jwt.PyJWTError:
68
+ return None
69
+
70
+
71
+ async def introspect_token(token: str) -> Optional[dict[str, Any]]:
72
+ """Call auth service introspect endpoint. Return payload if active, else None."""
73
+ base = get_auth_service_url()
74
+ path = get_auth_service_introspect_path()
75
+ if not base or not path:
76
+ return None
77
+ url = f"{base.rstrip('/')}{path}"
78
+ try:
79
+ import httpx
80
+ async with httpx.AsyncClient() as client:
81
+ # Common pattern: POST with token in body or header
82
+ r = await client.post(
83
+ url,
84
+ data={"token": token},
85
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
86
+ timeout=5.0,
87
+ )
88
+ if r.status_code != 200:
89
+ return None
90
+ data = r.json()
91
+ # Typical introspect: {"active": true, "sub": "username", ...}
92
+ if not data.get("active", False):
93
+ return None
94
+ return data
95
+ except Exception:
96
+ return None
97
+
98
+
99
+ async def get_current_user(
100
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
101
+ ) -> dict[str, Any]:
102
+ """
103
+ Resolve current user from Bearer token. Uses JWT verify or auth service introspect.
104
+ When auth is disabled, returns a dummy user so routes do not need to branch.
105
+ """
106
+ if is_auth_disabled():
107
+ return {"username": "anonymous", "auth_disabled": True}
108
+
109
+ token = None
110
+ if credentials and credentials.credentials:
111
+ token = credentials.credentials
112
+
113
+ if not token:
114
+ raise HTTPException(
115
+ status_code=status.HTTP_401_UNAUTHORIZED,
116
+ detail="Not authenticated",
117
+ headers={"WWW-Authenticate": "Bearer"},
118
+ )
119
+
120
+ # Prefer JWT verify (no HTTP); fall back to introspect if configured
121
+ payload = verify_jwt(token)
122
+ if payload is None and (get_auth_service_url() and get_auth_service_introspect_path()):
123
+ payload = await introspect_token(token)
124
+
125
+ if payload is None:
126
+ raise HTTPException(
127
+ status_code=status.HTTP_401_UNAUTHORIZED,
128
+ detail="Invalid or expired token",
129
+ headers={"WWW-Authenticate": "Bearer"},
130
+ )
131
+
132
+ username = payload.get("sub") or payload.get("username")
133
+ if not username:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_401_UNAUTHORIZED,
136
+ detail="Invalid token payload",
137
+ headers={"WWW-Authenticate": "Bearer"},
138
+ )
139
+
140
+ return {"username": username}
141
+
142
+
143
+ async def get_optional_user(
144
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
145
+ ) -> Optional[dict[str, Any]]:
146
+ """Like get_current_user but returns None instead of 401 when no/invalid token. Useful for public endpoints."""
147
+ if is_auth_disabled():
148
+ return {"username": "anonymous", "auth_disabled": True}
149
+ token = credentials.credentials if credentials and credentials.credentials else None
150
+ if not token:
151
+ return None
152
+ payload = verify_jwt(token)
153
+ if payload is None and (get_auth_service_url() and get_auth_service_introspect_path()):
154
+ payload = await introspect_token(token)
155
+ if payload is None:
156
+ return None
157
+ username = payload.get("sub") or payload.get("username")
158
+ return {"username": username} if username else None
pycharter/api/main.py CHANGED
@@ -17,6 +17,7 @@ from pycharter import __version__ as pycharter_version
17
17
 
18
18
  # Import routers from v1
19
19
  from pycharter.api.routes.v1 import (
20
+ auth,
20
21
  contracts,
21
22
  metadata,
22
23
  quality,
@@ -27,7 +28,10 @@ from pycharter.api.routes.v1 import (
27
28
  docs,
28
29
  tracking,
29
30
  evolution,
31
+ etl,
30
32
  )
33
+ from pycharter.api.dependencies.auth import get_current_user
34
+ from fastapi import Depends
31
35
 
32
36
  # Try to import validation_jobs router (requires worker component)
33
37
  try:
@@ -102,57 +106,80 @@ def create_application() -> FastAPI:
102
106
  allow_headers=["*"],
103
107
  )
104
108
 
105
- # Include routers from v1 with automatic /api/v1 prefix
106
- # All routers in routes/v1/ are automatically included with the /api/v1 prefix
109
+ # Auth router: no global auth dependency (login/logout public; /auth/me uses Depends in route)
110
+ app.include_router(
111
+ auth.router,
112
+ prefix=f"/api/{API_VERSION}",
113
+ tags=["Auth"],
114
+ )
115
+
116
+ # Protected routers: require valid Bearer token (when auth enabled; when disabled, get_current_user returns dummy user)
117
+ _auth_dep = [Depends(get_current_user)]
107
118
  app.include_router(
108
119
  contracts.router,
109
120
  prefix=f"/api/{API_VERSION}",
110
121
  tags=["Contracts"],
122
+ dependencies=_auth_dep,
111
123
  )
112
124
  app.include_router(
113
125
  metadata.router,
114
126
  prefix=f"/api/{API_VERSION}",
115
127
  tags=["Metadata"],
128
+ dependencies=_auth_dep,
116
129
  )
117
130
  app.include_router(
118
131
  schemas.router,
119
132
  prefix=f"/api/{API_VERSION}",
120
133
  tags=["Schemas"],
134
+ dependencies=_auth_dep,
121
135
  )
122
136
  app.include_router(
123
137
  validation.router,
124
138
  prefix=f"/api/{API_VERSION}",
125
139
  tags=["Validation"],
140
+ dependencies=_auth_dep,
126
141
  )
127
142
  app.include_router(
128
143
  quality.router,
129
144
  prefix=f"/api/{API_VERSION}",
130
145
  tags=["Quality"],
146
+ dependencies=_auth_dep,
131
147
  )
132
148
  app.include_router(
133
149
  templates.router,
134
150
  prefix=f"/api/{API_VERSION}",
135
151
  tags=["Templates"],
152
+ dependencies=_auth_dep,
153
+ )
154
+ app.include_router(
155
+ etl.router,
156
+ prefix=f"/api/{API_VERSION}",
157
+ tags=["ETL"],
158
+ dependencies=_auth_dep,
136
159
  )
137
160
  app.include_router(
138
161
  settings.router,
139
162
  prefix=f"/api/{API_VERSION}",
140
163
  tags=["Settings"],
164
+ dependencies=_auth_dep,
141
165
  )
142
166
  app.include_router(
143
167
  docs.router,
144
168
  prefix=f"/api/{API_VERSION}",
145
169
  tags=["Documentation"],
170
+ dependencies=_auth_dep,
146
171
  )
147
172
  app.include_router(
148
173
  tracking.router,
149
174
  prefix=f"/api/{API_VERSION}",
150
175
  tags=["Quality Tracking"],
176
+ dependencies=_auth_dep,
151
177
  )
152
178
  app.include_router(
153
179
  evolution.router,
154
180
  prefix=f"/api/{API_VERSION}",
155
181
  tags=["Schema Evolution"],
182
+ dependencies=_auth_dep,
156
183
  )
157
184
 
158
185
  # Include validation_jobs router if worker component is available
@@ -161,6 +188,7 @@ def create_application() -> FastAPI:
161
188
  validation_jobs.router,
162
189
  prefix=f"/api/{API_VERSION}",
163
190
  tags=["Validation Jobs"],
191
+ dependencies=_auth_dep,
164
192
  )
165
193
 
166
194
  # Root endpoint
@@ -0,0 +1,66 @@
1
+ """
2
+ Request/Response models for ETL run endpoint.
3
+ """
4
+
5
+ from typing import Dict, List, Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class EtlRunRequest(BaseModel):
11
+ """Request model for running an ETL pipeline from YAML configs."""
12
+
13
+ extract_yaml: str = Field(..., description="Extract config as YAML string")
14
+ load_yaml: str = Field(..., description="Load config as YAML string")
15
+ transform_yaml: Optional[str] = Field(
16
+ default=None,
17
+ description="Optional transform config as YAML string",
18
+ )
19
+ variables: Optional[Dict[str, str]] = Field(
20
+ default=None,
21
+ description="Optional variables for ${VAR} substitution in configs",
22
+ )
23
+ dry_run: bool = Field(
24
+ default=False,
25
+ description="If True, extract and transform but do not load",
26
+ )
27
+
28
+
29
+ class EtlRunResponse(BaseModel):
30
+ """Response model for ETL pipeline run result (mirrors PipelineResult.to_dict())."""
31
+
32
+ success: bool = Field(..., description="Whether the pipeline run succeeded")
33
+ rows_extracted: int = Field(0, description="Number of rows extracted")
34
+ rows_transformed: int = Field(0, description="Number of rows transformed")
35
+ rows_loaded: int = Field(0, description="Number of rows loaded")
36
+ rows_failed: int = Field(0, description="Number of rows that failed to load")
37
+ # Validation tracking
38
+ rows_quarantined_extract: int = Field(
39
+ 0,
40
+ description="Number of rows quarantined at extract stage due to validation",
41
+ )
42
+ rows_quarantined_load: int = Field(
43
+ 0,
44
+ description="Number of rows quarantined at load stage due to validation",
45
+ )
46
+ total_quarantined: int = Field(
47
+ 0,
48
+ description="Total rows quarantined at both stages",
49
+ )
50
+ validation_errors_extract: List[str] = Field(
51
+ default_factory=list,
52
+ description="Validation error messages from extract stage",
53
+ )
54
+ validation_errors_load: List[str] = Field(
55
+ default_factory=list,
56
+ description="Validation error messages from load stage",
57
+ )
58
+ # Timing and metadata
59
+ duration_seconds: Optional[float] = Field(
60
+ None,
61
+ description="Total run duration in seconds",
62
+ )
63
+ batches_processed: int = Field(0, description="Number of batches processed")
64
+ errors: List[str] = Field(default_factory=list, description="Error messages if any")
65
+ pipeline_name: Optional[str] = Field(None, description="Pipeline name if set")
66
+ run_id: Optional[str] = Field(None, description="Run identifier")
@@ -7,6 +7,7 @@ with the `/api/v1` prefix.
7
7
  """
8
8
 
9
9
  from pycharter.api.routes.v1 import (
10
+ auth,
10
11
  contracts,
11
12
  metadata,
12
13
  quality,
@@ -16,10 +17,12 @@ from pycharter.api.routes.v1 import (
16
17
  docs,
17
18
  tracking,
18
19
  evolution,
20
+ etl,
19
21
  )
20
22
 
21
23
  # Export all routers for automatic inclusion in main.py
22
24
  __all__ = [
25
+ "auth",
23
26
  "contracts",
24
27
  "metadata",
25
28
  "quality",
@@ -29,5 +32,6 @@ __all__ = [
29
32
  "docs",
30
33
  "tracking",
31
34
  "evolution",
35
+ "etl",
32
36
  ]
33
37
 
@@ -0,0 +1,97 @@
1
+ """
2
+ Authentication routes: login, logout, me.
3
+ """
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, status
6
+ from pydantic import BaseModel
7
+
8
+ from pycharter.api.dependencies.auth import (
9
+ create_access_token,
10
+ get_current_user,
11
+ verify_initial_credentials,
12
+ ACCESS_TOKEN_EXPIRE_SECONDS,
13
+ )
14
+ from pycharter.config import (
15
+ get_auth_initial_credentials,
16
+ get_auth_jwt_secret,
17
+ is_auth_disabled,
18
+ )
19
+
20
+ router = APIRouter()
21
+
22
+
23
+ class LoginRequest(BaseModel):
24
+ username: str
25
+ password: str
26
+
27
+
28
+ class LoginResponse(BaseModel):
29
+ access_token: str
30
+ token_type: str = "bearer"
31
+ expires_in: int = ACCESS_TOKEN_EXPIRE_SECONDS
32
+
33
+
34
+ class UserResponse(BaseModel):
35
+ username: str
36
+ auth_disabled: bool = False
37
+
38
+
39
+ @router.post(
40
+ "/auth/login",
41
+ response_model=LoginResponse,
42
+ status_code=status.HTTP_200_OK,
43
+ summary="Login",
44
+ description="Exchange username and password for an access token. Uses initial credentials (env or pycharter.cfg) when configured.",
45
+ )
46
+ def login(request: LoginRequest) -> LoginResponse:
47
+ if is_auth_disabled():
48
+ raise HTTPException(
49
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
50
+ detail="Authentication is disabled",
51
+ )
52
+ creds = get_auth_initial_credentials()
53
+ if not creds:
54
+ raise HTTPException(
55
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
56
+ detail="Authentication not configured (set initial credentials or auth service)",
57
+ )
58
+ if not get_auth_jwt_secret():
59
+ raise HTTPException(
60
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
61
+ detail="JWT secret not configured (set PYCHARTER_AUTH_JWT_SECRET or auth.jwt_secret)",
62
+ )
63
+ if not verify_initial_credentials(request.username, request.password):
64
+ raise HTTPException(
65
+ status_code=status.HTTP_401_UNAUTHORIZED,
66
+ detail="Invalid username or password",
67
+ )
68
+ token = create_access_token(request.username)
69
+ return LoginResponse(
70
+ access_token=token,
71
+ token_type="bearer",
72
+ expires_in=ACCESS_TOKEN_EXPIRE_SECONDS,
73
+ )
74
+
75
+
76
+ @router.post(
77
+ "/auth/logout",
78
+ status_code=status.HTTP_200_OK,
79
+ summary="Logout",
80
+ description="Client should discard the token. Server-side invalidation can be added later.",
81
+ )
82
+ def logout() -> dict:
83
+ return {"message": "OK"}
84
+
85
+
86
+ @router.get(
87
+ "/auth/me",
88
+ response_model=UserResponse,
89
+ status_code=status.HTTP_200_OK,
90
+ summary="Current user",
91
+ description="Return the current user from the Bearer token. Protected.",
92
+ )
93
+ def me(current_user: dict = Depends(get_current_user)) -> UserResponse:
94
+ return UserResponse(
95
+ username=current_user["username"],
96
+ auth_disabled=current_user.get("auth_disabled", False),
97
+ )
@@ -269,10 +269,11 @@ async def build_contract_from_id_endpoint(
269
269
  model_name="CoercionRule"
270
270
  )
271
271
  if coercion_rules and coercion_rules.rules:
272
- # Format coercion rules - the rules dict itself, with version if needed
273
- # The builder will extract the "rules" key if present, otherwise uses the whole dict
274
- coercion_rules_data = dict(coercion_rules.rules) if coercion_rules.rules else {}
275
- coercion_rules_data["version"] = coercion_rules.version
272
+ # Builder expects {"version": "...", "rules": {...}} so _extract_rules returns the inner dict
273
+ coercion_rules_data = {
274
+ "version": coercion_rules.version,
275
+ "rules": dict(coercion_rules.rules),
276
+ }
276
277
 
277
278
  # Get validation rules (optional)
278
279
  validation_rules_data = None
@@ -282,10 +283,11 @@ async def build_contract_from_id_endpoint(
282
283
  model_name="ValidationRule"
283
284
  )
284
285
  if validation_rules and validation_rules.rules:
285
- # Format validation rules - the rules dict itself, with version if needed
286
- # The builder will extract the "rules" key if present, otherwise uses the whole dict
287
- validation_rules_data = dict(validation_rules.rules) if validation_rules.rules else {}
288
- validation_rules_data["version"] = validation_rules.version
286
+ # Builder expects {"version": "...", "rules": {...}} so _extract_rules returns the inner dict
287
+ validation_rules_data = {
288
+ "version": validation_rules.version,
289
+ "rules": dict(validation_rules.rules),
290
+ }
289
291
 
290
292
  # Get metadata (optional)
291
293
  metadata_data = None
@@ -0,0 +1,131 @@
1
+ """
2
+ Route handlers for ETL pipeline execution.
3
+
4
+ Allows running an ETL pipeline from YAML config strings (extract, transform, load).
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Union
9
+
10
+ import yaml
11
+ from fastapi import APIRouter, HTTPException, status
12
+
13
+ from pycharter import Pipeline
14
+ from pycharter.api.models.etl import EtlRunRequest, EtlRunResponse
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ def _parse_yaml_config(yaml_str: str, field_name: str) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
22
+ """
23
+ Parse a YAML string into a dict or list of dicts.
24
+ Empty or whitespace-only string returns {}.
25
+ Raises HTTPException 400 on parse error or invalid type.
26
+ """
27
+ if not yaml_str or not yaml_str.strip():
28
+ return {}
29
+ try:
30
+ parsed = yaml.safe_load(yaml_str)
31
+ except yaml.YAMLError as e:
32
+ raise HTTPException(
33
+ status_code=status.HTTP_400_BAD_REQUEST,
34
+ detail=f"Invalid YAML in {field_name}: {str(e)}",
35
+ )
36
+ if parsed is None:
37
+ return {}
38
+ if isinstance(parsed, dict):
39
+ return parsed
40
+ if isinstance(parsed, list):
41
+ if all(isinstance(item, dict) for item in parsed):
42
+ return parsed
43
+ raise HTTPException(
44
+ status_code=status.HTTP_400_BAD_REQUEST,
45
+ detail=f"{field_name} must be a YAML object or list of objects",
46
+ )
47
+ raise HTTPException(
48
+ status_code=status.HTTP_400_BAD_REQUEST,
49
+ detail=f"{field_name} must be a YAML object or list of objects, got {type(parsed).__name__}",
50
+ )
51
+
52
+
53
+ @router.post(
54
+ "/etl/run",
55
+ response_model=EtlRunResponse,
56
+ status_code=status.HTTP_200_OK,
57
+ summary="Run ETL pipeline",
58
+ description="Run an ETL pipeline from extract, transform, and load YAML config strings. Optional variables are applied for ${VAR} substitution. Use dry_run=True to extract and transform without loading.",
59
+ response_description="Pipeline run result with row counts and any errors",
60
+ tags=["ETL"],
61
+ )
62
+ async def run_etl_pipeline(request: EtlRunRequest) -> EtlRunResponse:
63
+ """
64
+ Run an ETL pipeline from the provided YAML configs.
65
+
66
+ Parses extract_yaml, load_yaml, and optional transform_yaml; builds a pipeline
67
+ via Pipeline.from_dict() (so variable substitution is applied), runs it, and
68
+ returns the result (row counts, success, errors).
69
+ """
70
+ # Parse YAML configs
71
+ extract_dict = _parse_yaml_config(request.extract_yaml, "extract")
72
+ load_dict = _parse_yaml_config(request.load_yaml, "load")
73
+ transform_raw = _parse_yaml_config(request.transform_yaml or "", "transform")
74
+
75
+ if not isinstance(extract_dict, dict) or not extract_dict:
76
+ raise HTTPException(
77
+ status_code=status.HTTP_400_BAD_REQUEST,
78
+ detail="Extract config is required and must be a non-empty YAML object",
79
+ )
80
+ if not isinstance(load_dict, dict) or not load_dict:
81
+ raise HTTPException(
82
+ status_code=status.HTTP_400_BAD_REQUEST,
83
+ detail="Load config is required and must be a non-empty YAML object",
84
+ )
85
+
86
+ # Build config for Pipeline.from_dict
87
+ # The transform YAML can contain: transform (simple ops), jsonata, custom_function
88
+ # We pass the full parsed dict to from_dict which will extract these
89
+ config: Dict[str, Any] = {
90
+ "extract": extract_dict,
91
+ "load": load_dict,
92
+ }
93
+
94
+ # If transform_raw is a dict, include its keys in config (transform, jsonata, custom_function)
95
+ if isinstance(transform_raw, dict):
96
+ if "transform" in transform_raw:
97
+ config["transform"] = transform_raw["transform"]
98
+ if "jsonata" in transform_raw:
99
+ config["jsonata"] = transform_raw["jsonata"]
100
+ if "custom_function" in transform_raw:
101
+ config["custom_function"] = transform_raw["custom_function"]
102
+ elif isinstance(transform_raw, list):
103
+ # If it's a list, treat it as transform steps
104
+ config["transform"] = transform_raw
105
+
106
+ variables = request.variables or {}
107
+
108
+ try:
109
+ pipeline = Pipeline.from_dict(config, variables=variables)
110
+ except ValueError as e:
111
+ raise HTTPException(
112
+ status_code=status.HTTP_400_BAD_REQUEST,
113
+ detail=str(e),
114
+ )
115
+ except Exception as e:
116
+ logger.exception("Pipeline build failed")
117
+ raise HTTPException(
118
+ status_code=status.HTTP_400_BAD_REQUEST,
119
+ detail=f"Pipeline config invalid: {str(e)}",
120
+ )
121
+
122
+ try:
123
+ result = await pipeline.run(dry_run=request.dry_run)
124
+ except Exception as e:
125
+ logger.exception("Pipeline run failed")
126
+ raise HTTPException(
127
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
128
+ detail=str(e),
129
+ )
130
+
131
+ return EtlRunResponse(**result.to_dict())
pycharter/cli.py CHANGED
@@ -127,7 +127,7 @@ def main():
127
127
  seed_parser.add_argument(
128
128
  "seed_dir",
129
129
  nargs="?",
130
- help="Directory containing seed YAML files (default: data/seed)",
130
+ help="Directory containing seed YAML files (default: package data/seed)",
131
131
  )
132
132
  seed_parser.add_argument(
133
133
  "database_url",