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,416 @@
1
+ """
2
+ MongoDB loader for ETL orchestrator.
3
+
4
+ Supports loading data to MongoDB collections using:
5
+ - insert: Insert new documents (fails on duplicates)
6
+ - upsert: Insert or update based on primary key
7
+ - replace: Replace entire documents
8
+ - update: Update specific fields in existing documents
9
+ - delete: Delete documents by primary key
10
+
11
+ Configuration example:
12
+ ```yaml
13
+ type: mongodb
14
+ mongodb:
15
+ url: mongodb://user:pass@localhost:27017
16
+ database: mydb
17
+ collection: users
18
+ ssh_tunnel:
19
+ enabled: false
20
+ host: bastion.example.com
21
+ username: tunnel_user
22
+ remote_host: mongo-internal.example.com
23
+ remote_port: 27017
24
+
25
+ write_method: upsert # insert, upsert, replace, update, delete
26
+ primary_key: _id # field to use for matching (default: _id)
27
+
28
+ # Optional: ordered bulk writes (default: true)
29
+ # Set to false for better performance when order doesn't matter
30
+ ordered: false
31
+
32
+ # Optional: bypass document validation
33
+ bypass_validation: false
34
+ ```
35
+ """
36
+
37
+ import logging
38
+ import time
39
+ from typing import Any, Dict, List, Optional, Union
40
+
41
+ from bson import ObjectId
42
+ from pymongo import MongoClient
43
+ from pymongo.operations import (
44
+ DeleteOne,
45
+ InsertOne,
46
+ ReplaceOne,
47
+ UpdateOne,
48
+ )
49
+
50
+ from pycharter.etl_generator.loaders.base import BaseLoader
51
+ from pycharter.etl_generator.result import LoadResult
52
+ from pycharter.etl_generator.database import (
53
+ create_ssh_tunnel,
54
+ DEFAULT_TUNNEL_LOCAL_PORT,
55
+ )
56
+ from pycharter.utils.value_injector import resolve_values
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ # Default MongoDB port
61
+ DEFAULT_MONGODB_PORT = 27017
62
+
63
+
64
+ def _modify_mongodb_url_for_tunnel(url: str, local_port: int) -> str:
65
+ """
66
+ Modify MongoDB URL to use local tunnel port.
67
+
68
+ Args:
69
+ url: Original MongoDB URL
70
+ local_port: Local tunnel port
71
+
72
+ Returns:
73
+ Modified URL pointing to localhost tunnel
74
+ """
75
+ import re
76
+ pattern = r'mongodb(\+srv)?://([^@]+@)?([^/:]+)(:\d+)?'
77
+
78
+ def replace_host(match):
79
+ protocol = match.group(1) or ''
80
+ auth = match.group(2) or ''
81
+ return f'mongodb://{auth}127.0.0.1:{local_port}'
82
+
83
+ return re.sub(pattern, replace_host, url)
84
+
85
+
86
+ def _convert_id_field(doc: Dict[str, Any], primary_key: str) -> Dict[str, Any]:
87
+ """
88
+ Convert primary key to ObjectId if it's a valid ObjectId string.
89
+
90
+ Args:
91
+ doc: Document dictionary
92
+ primary_key: Primary key field name
93
+
94
+ Returns:
95
+ Document with converted _id if applicable
96
+ """
97
+ result = doc.copy()
98
+
99
+ # If primary key is '_id' and value is a valid ObjectId string, convert it
100
+ if primary_key == '_id' and '_id' in result:
101
+ id_value = result['_id']
102
+ if isinstance(id_value, str) and ObjectId.is_valid(id_value):
103
+ result['_id'] = ObjectId(id_value)
104
+
105
+ return result
106
+
107
+
108
+ class MongoDBLoader(BaseLoader):
109
+ """
110
+ Loader for MongoDB data targets.
111
+
112
+ Supports:
113
+ - Insert (insert_many for bulk inserts)
114
+ - Upsert (bulk_write with ReplaceOne operations)
115
+ - Replace (bulk_write with ReplaceOne)
116
+ - Update (bulk_write with UpdateOne)
117
+ - Delete (bulk_write with DeleteOne)
118
+
119
+ Example (programmatic API):
120
+ >>> loader = MongoDBLoader(
121
+ ... connection_string="mongodb://localhost:27017",
122
+ ... database="mydb",
123
+ ... collection="users",
124
+ ... write_method="upsert",
125
+ ... primary_key="_id",
126
+ ... )
127
+ >>> result = await loader.load(data)
128
+
129
+ Example (config-driven):
130
+ >>> loader = MongoDBLoader.from_config(config)
131
+ >>> result = await loader.load(data)
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ connection_string: str,
137
+ database: str,
138
+ collection: str,
139
+ write_method: str = "upsert",
140
+ primary_key: Union[str, List[str]] = "_id",
141
+ batch_size: int = 1000,
142
+ ordered: bool = True,
143
+ bypass_validation: bool = False,
144
+ ssh_tunnel: Optional[Dict[str, Any]] = None,
145
+ ):
146
+ """
147
+ Initialize MongoDB loader.
148
+
149
+ Args:
150
+ connection_string: MongoDB connection URL
151
+ database: Database name
152
+ collection: Collection name
153
+ write_method: Write method (insert, upsert, replace, update, delete)
154
+ primary_key: Field(s) to use for matching documents
155
+ batch_size: Number of operations per bulk write
156
+ ordered: Whether bulk writes should be ordered
157
+ bypass_validation: Whether to bypass document validation
158
+ ssh_tunnel: SSH tunnel configuration
159
+ """
160
+ self.connection_string = connection_string
161
+ self.database = database
162
+ self.collection = collection
163
+ self.write_method = write_method
164
+ self.primary_key = primary_key
165
+ self.batch_size = batch_size
166
+ self.ordered = ordered
167
+ self.bypass_validation = bypass_validation
168
+ self.ssh_tunnel = ssh_tunnel
169
+
170
+ @classmethod
171
+ def from_config(cls, config: Dict[str, Any]) -> "MongoDBLoader":
172
+ """Create loader from configuration dict."""
173
+ mongo_config = config.get("mongodb", {})
174
+ return cls(
175
+ connection_string=mongo_config.get("url") or config.get("connection_string"),
176
+ database=mongo_config.get("database") or config.get("database"),
177
+ collection=mongo_config.get("collection") or config.get("collection"),
178
+ write_method=config.get("write_method", "upsert"),
179
+ primary_key=config.get("primary_key", "_id"),
180
+ batch_size=config.get("batch_size", 1000),
181
+ ordered=config.get("ordered", True),
182
+ bypass_validation=config.get("bypass_validation", False),
183
+ ssh_tunnel=mongo_config.get("ssh_tunnel"),
184
+ )
185
+
186
+ def _build_filter(self, doc: Dict[str, Any]) -> Dict[str, Any]:
187
+ """
188
+ Build a filter document for matching.
189
+
190
+ Args:
191
+ doc: Source document
192
+
193
+ Returns:
194
+ Filter document using primary key field(s)
195
+ """
196
+ if isinstance(self.primary_key, list):
197
+ # Composite key
198
+ return {key: doc.get(key) for key in self.primary_key if key in doc}
199
+ else:
200
+ # Single key
201
+ key_value = doc.get(self.primary_key)
202
+ if key_value is None:
203
+ raise ValueError(
204
+ f"Document missing primary key field '{self.primary_key}': {doc}"
205
+ )
206
+ return {self.primary_key: key_value}
207
+
208
+ async def load(self, data: List[Dict[str, Any]], **params) -> LoadResult:
209
+ """
210
+ Load data to MongoDB collection.
211
+
212
+ Args:
213
+ data: List of documents to load
214
+ **params: Additional parameters (config_context, contract_dir)
215
+
216
+ Returns:
217
+ LoadResult with statistics
218
+ """
219
+ start_time = time.time()
220
+
221
+ if not data:
222
+ return LoadResult(success=True, rows_loaded=0)
223
+
224
+ if not self.connection_string:
225
+ raise ValueError("MongoDB connection string is required")
226
+ if not self.database:
227
+ raise ValueError("MongoDB database name is required")
228
+ if not self.collection:
229
+ raise ValueError("MongoDB collection name is required")
230
+
231
+ # Handle SSH tunnel if configured
232
+ tunnel = None
233
+ connection_string = self.connection_string
234
+
235
+ config_context = params.get('config_context')
236
+ contract_dir = params.get('contract_dir')
237
+ source_file = str(contract_dir / "load.yaml") if contract_dir else None
238
+
239
+ if self.ssh_tunnel:
240
+ ssh_config = resolve_values(
241
+ self.ssh_tunnel,
242
+ context=config_context,
243
+ source_file=source_file
244
+ )
245
+ enabled_value = ssh_config.get('enabled', False)
246
+ if isinstance(enabled_value, str):
247
+ enabled_lower = enabled_value.lower()
248
+ ssh_config['enabled'] = enabled_lower in ('true', '1', 'yes', 'on')
249
+ elif not isinstance(enabled_value, bool):
250
+ ssh_config['enabled'] = bool(enabled_value)
251
+
252
+ if ssh_config.get('enabled', False):
253
+ if 'remote_port' not in ssh_config:
254
+ ssh_config['remote_port'] = DEFAULT_MONGODB_PORT
255
+
256
+ tunnel = create_ssh_tunnel(ssh_config)
257
+ if tunnel:
258
+ local_port = int(ssh_config.get('local_port', DEFAULT_TUNNEL_LOCAL_PORT))
259
+ connection_string = _modify_mongodb_url_for_tunnel(
260
+ connection_string, local_port
261
+ )
262
+
263
+ # Create MongoDB client
264
+ client = MongoClient(connection_string)
265
+ db = client[self.database]
266
+ collection = db[self.collection]
267
+
268
+ inserted = 0
269
+ updated = 0
270
+ deleted = 0
271
+ errors = []
272
+
273
+ try:
274
+ logger.info(
275
+ f"Loading to MongoDB: {self.database}.{self.collection} "
276
+ f"(method: {self.write_method}, records: {len(data)})"
277
+ )
278
+
279
+ # Process in batches
280
+ for i in range(0, len(data), self.batch_size):
281
+ batch = data[i:i + self.batch_size]
282
+
283
+ if self.write_method == 'insert':
284
+ # Simple insert_many
285
+ result = collection.insert_many(
286
+ batch,
287
+ ordered=self.ordered,
288
+ bypass_document_validation=self.bypass_validation,
289
+ )
290
+ inserted += len(result.inserted_ids)
291
+
292
+ elif self.write_method == 'upsert':
293
+ # Bulk upsert using ReplaceOne with upsert=True
294
+ operations = []
295
+ for doc in batch:
296
+ doc = _convert_id_field(doc, self.primary_key if isinstance(self.primary_key, str) else '_id')
297
+ filter_doc = self._build_filter(doc)
298
+ operations.append(
299
+ ReplaceOne(filter_doc, doc, upsert=True)
300
+ )
301
+
302
+ if operations:
303
+ result = collection.bulk_write(
304
+ operations,
305
+ ordered=self.ordered,
306
+ bypass_document_validation=self.bypass_validation,
307
+ )
308
+ inserted += result.upserted_count
309
+ updated += result.modified_count
310
+
311
+ elif self.write_method == 'replace':
312
+ # Bulk replace (no upsert - only updates existing)
313
+ operations = []
314
+ for doc in batch:
315
+ doc = _convert_id_field(doc, self.primary_key if isinstance(self.primary_key, str) else '_id')
316
+ filter_doc = self._build_filter(doc)
317
+ operations.append(
318
+ ReplaceOne(filter_doc, doc, upsert=False)
319
+ )
320
+
321
+ if operations:
322
+ result = collection.bulk_write(
323
+ operations,
324
+ ordered=self.ordered,
325
+ bypass_document_validation=self.bypass_validation,
326
+ )
327
+ updated += result.modified_count
328
+
329
+ elif self.write_method == 'update':
330
+ # Bulk update using $set
331
+ operations = []
332
+ for doc in batch:
333
+ doc = _convert_id_field(doc, self.primary_key if isinstance(self.primary_key, str) else '_id')
334
+ filter_doc = self._build_filter(doc)
335
+ # Remove primary key from update fields
336
+ update_fields = {
337
+ k: v for k, v in doc.items()
338
+ if k != self.primary_key and k not in (
339
+ self.primary_key if isinstance(self.primary_key, list) else [self.primary_key]
340
+ )
341
+ }
342
+ operations.append(
343
+ UpdateOne(filter_doc, {"$set": update_fields})
344
+ )
345
+
346
+ if operations:
347
+ result = collection.bulk_write(
348
+ operations,
349
+ ordered=self.ordered,
350
+ bypass_document_validation=self.bypass_validation,
351
+ )
352
+ updated += result.modified_count
353
+
354
+ elif self.write_method == 'delete':
355
+ # Bulk delete by primary key
356
+ operations = []
357
+ for doc in batch:
358
+ doc = _convert_id_field(doc, self.primary_key if isinstance(self.primary_key, str) else '_id')
359
+ filter_doc = self._build_filter(doc)
360
+ operations.append(DeleteOne(filter_doc))
361
+
362
+ if operations:
363
+ result = collection.bulk_write(
364
+ operations,
365
+ ordered=self.ordered,
366
+ )
367
+ deleted += result.deleted_count
368
+
369
+ elif self.write_method == 'truncate_and_load':
370
+ # Delete all documents first (only on first batch)
371
+ if i == 0:
372
+ collection.delete_many({})
373
+ logger.info(f"Truncated collection {self.database}.{self.collection}")
374
+
375
+ # Then insert
376
+ result = collection.insert_many(
377
+ batch,
378
+ ordered=self.ordered,
379
+ bypass_document_validation=self.bypass_validation,
380
+ )
381
+ inserted += len(result.inserted_ids)
382
+
383
+ else:
384
+ raise ValueError(f"Unsupported write method: {self.write_method}")
385
+
386
+ duration = time.time() - start_time
387
+ total_affected = inserted + updated + deleted
388
+
389
+ logger.info(
390
+ f"MongoDB load completed: {total_affected} records affected "
391
+ f"(inserted: {inserted}, updated: {updated}, deleted: {deleted}) "
392
+ f"in {duration:.2f}s"
393
+ )
394
+
395
+ return LoadResult(
396
+ success=True,
397
+ rows_loaded=inserted + updated,
398
+ duration_seconds=duration,
399
+ metadata={
400
+ "inserted": inserted,
401
+ "updated": updated,
402
+ "deleted": deleted,
403
+ },
404
+ )
405
+
406
+ except Exception as e:
407
+ logger.error(f"MongoDB load failed: {e}", exc_info=True)
408
+ return LoadResult(
409
+ success=False,
410
+ error=str(e),
411
+ duration_seconds=time.time() - start_time,
412
+ )
413
+ finally:
414
+ client.close()
415
+ if tunnel:
416
+ tunnel.stop()