mage-ai 0.9.68__py3-none-any.whl → 0.9.69__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.

Potentially problematic release.


This version of mage-ai might be problematic. Click here for more details.

Files changed (231) hide show
  1. mage_ai/api/resources/SeedResource.py +2 -1
  2. mage_ai/api/resources/SessionResource.py +13 -1
  3. mage_ai/authentication/permissions/constants.py +2 -0
  4. mage_ai/authentication/permissions/seed.py +32 -21
  5. mage_ai/authentication/providers/active_directory.py +4 -3
  6. mage_ai/authentication/providers/okta.py +22 -83
  7. mage_ai/data_preparation/executors/streaming_pipeline_executor.py +77 -7
  8. mage_ai/data_preparation/models/block/__init__.py +7 -3
  9. mage_ai/data_preparation/models/block/data_integration/mixins.py +16 -5
  10. mage_ai/data_preparation/models/block/dynamic/child.py +3 -0
  11. mage_ai/data_preparation/models/block/dynamic/variables.py +2 -2
  12. mage_ai/data_preparation/models/block/extension/utils.py +1 -0
  13. mage_ai/data_preparation/models/block/integration/__init__.py +1 -1
  14. mage_ai/data_preparation/models/block/sql/__init__.py +1 -1
  15. mage_ai/data_preparation/models/pipeline.py +22 -6
  16. mage_ai/data_preparation/models/utils.py +6 -0
  17. mage_ai/data_preparation/models/variable.py +18 -4
  18. mage_ai/data_preparation/repo_manager.py +3 -2
  19. mage_ai/data_preparation/shared/utils.py +1 -1
  20. mage_ai/data_preparation/templates/data_exporters/mysql.py +2 -2
  21. mage_ai/data_preparation/templates/data_exporters/oracledb.py +27 -0
  22. mage_ai/data_preparation/templates/repo/metadata.yaml +1 -0
  23. mage_ai/io/bigquery.py +131 -58
  24. mage_ai/io/mysql.py +38 -6
  25. mage_ai/io/snowflake.py +152 -29
  26. mage_ai/orchestration/db/models/oauth.py +4 -4
  27. mage_ai/orchestration/db/models/schedules.py +9 -2
  28. mage_ai/orchestration/job_manager.py +6 -0
  29. mage_ai/orchestration/pipeline_scheduler_original.py +16 -6
  30. mage_ai/orchestration/queue/celery_queue.py +8 -1
  31. mage_ai/orchestration/queue/process_queue.py +67 -4
  32. mage_ai/orchestration/queue/queue.py +8 -0
  33. mage_ai/server/constants.py +1 -1
  34. mage_ai/server/frontend_dist/404.html +2 -2
  35. mage_ai/server/frontend_dist/_next/static/{i8pymuJDTVHdWjUP1QSh1 → _krrrgup_C-dPOpX36S8I}/_buildManifest.js +1 -1
  36. mage_ai/server/frontend_dist/_next/static/chunks/1557-df144fbd8b2208c3.js +1 -0
  37. mage_ai/server/frontend_dist/_next/static/chunks/2717-d9200be634dd6766.js +1 -0
  38. mage_ai/server/frontend_dist/_next/static/chunks/3548-fa0792ddb88f4646.js +1 -0
  39. mage_ai/server/frontend_dist/_next/static/chunks/5699-6d708c6b2153ea08.js +1 -0
  40. mage_ai/server/frontend_dist/_next/static/chunks/7361-8a23dd8360593e7a.js +1 -0
  41. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-d9c89527266296f7.js +1 -0
  42. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-4e12783b064c1cfe.js +1 -0
  43. mage_ai/server/frontend_dist/_next/static/chunks/pages/{pipeline-runs-a66b4c7641ae03eb.js → pipeline-runs-3edc6270c5b0e962.js} +1 -1
  44. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-e1dd1ed71d26c10d.js +1 -0
  45. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-59aca25a5b1d3998.js +1 -0
  46. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers-cb88fd075a357fcf.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers-1bdfda8edc9cf4a8.js} +1 -1
  47. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-3591d035bb3bb2b8.js +1 -0
  48. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-503049734a8b082f.js +1 -0
  49. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-c2e9ef989c8bfa73.js +1 -0
  50. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-5b26eeda8aed8a7b.js +1 -0
  51. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{sync-data-79a4cf66a623e667.js → sync-data-8b793b3b696a2cd3.js} +1 -1
  52. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{users-86814e581acaf5db.js → users-a4db8710f703c729.js} +1 -1
  53. mage_ai/server/frontend_dist/_next/static/chunks/pages/sign-in-09414a8b66fb6f06.js +1 -0
  54. mage_ai/server/frontend_dist/block-layout.html +2 -2
  55. mage_ai/server/frontend_dist/compute.html +2 -2
  56. mage_ai/server/frontend_dist/files.html +2 -2
  57. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  58. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  59. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  60. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  61. mage_ai/server/frontend_dist/index.html +2 -2
  62. mage_ai/server/frontend_dist/manage/files.html +2 -2
  63. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  64. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  65. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  66. mage_ai/server/frontend_dist/manage/users.html +2 -2
  67. mage_ai/server/frontend_dist/manage.html +2 -2
  68. mage_ai/server/frontend_dist/oauth.html +2 -2
  69. mage_ai/server/frontend_dist/overview.html +2 -2
  70. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  71. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  72. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  73. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  74. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  75. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  76. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  77. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  78. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  79. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  80. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  81. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  82. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  83. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  84. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  85. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  86. mage_ai/server/frontend_dist/pipelines.html +2 -2
  87. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
  88. mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
  89. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  90. mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
  91. mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
  92. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  93. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  94. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  95. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  96. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  97. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  98. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  99. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  100. mage_ai/server/frontend_dist/settings.html +2 -2
  101. mage_ai/server/frontend_dist/sign-in.html +2 -2
  102. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  103. mage_ai/server/frontend_dist/templates.html +2 -2
  104. mage_ai/server/frontend_dist/terminal.html +2 -2
  105. mage_ai/server/frontend_dist/test.html +2 -2
  106. mage_ai/server/frontend_dist/triggers.html +2 -2
  107. mage_ai/server/frontend_dist/version-control.html +2 -2
  108. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  109. mage_ai/server/frontend_dist_base_path_template/_next/static/{CKCvjsYCf2imD2X8zAOBf → KLL5mirre9d7_ZeEpaw3s}/_buildManifest.js +1 -1
  110. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-df144fbd8b2208c3.js +1 -0
  111. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-d9200be634dd6766.js +1 -0
  112. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-fa0792ddb88f4646.js +1 -0
  113. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5699-6d708c6b2153ea08.js +1 -0
  114. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-8a23dd8360593e7a.js +1 -0
  115. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-d9c89527266296f7.js +1 -0
  116. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-4e12783b064c1cfe.js +1 -0
  117. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{pipeline-runs-a66b4c7641ae03eb.js → pipeline-runs-3edc6270c5b0e962.js} +1 -1
  118. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-e1dd1ed71d26c10d.js +1 -0
  119. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-59aca25a5b1d3998.js +1 -0
  120. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers-cb88fd075a357fcf.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers-1bdfda8edc9cf4a8.js} +1 -1
  121. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-3591d035bb3bb2b8.js +1 -0
  122. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-503049734a8b082f.js +1 -0
  123. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-c2e9ef989c8bfa73.js +1 -0
  124. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-5b26eeda8aed8a7b.js +1 -0
  125. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{sync-data-79a4cf66a623e667.js → sync-data-8b793b3b696a2cd3.js} +1 -1
  126. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{users-86814e581acaf5db.js → users-a4db8710f703c729.js} +1 -1
  127. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/sign-in-09414a8b66fb6f06.js +1 -0
  128. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  129. mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
  130. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  131. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  132. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  133. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
  134. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
  135. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  136. mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
  137. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  138. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  139. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  140. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  141. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  142. mage_ai/server/frontend_dist_base_path_template/oauth.html +2 -2
  143. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  144. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  145. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  146. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  147. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
  148. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  149. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  150. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  151. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  152. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  153. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  154. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  155. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  156. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  157. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  158. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  159. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  160. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  161. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
  162. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
  163. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  164. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
  165. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
  166. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
  167. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
  168. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  169. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
  170. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
  171. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  172. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
  173. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  174. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  175. mage_ai/server/frontend_dist_base_path_template/sign-in.html +2 -2
  176. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  177. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  178. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  179. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  180. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  181. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  182. mage_ai/server/scheduler_manager.py +7 -0
  183. mage_ai/server/server.py +12 -5
  184. mage_ai/server/websocket_server.py +1 -0
  185. mage_ai/settings/keys/auth.py +2 -0
  186. mage_ai/settings/server.py +1 -1
  187. mage_ai/streaming/sources/influxdb.py +2 -0
  188. mage_ai/streaming/sources/kafka.py +1 -1
  189. mage_ai/tests/api/endpoints/mixins.py +10 -9
  190. mage_ai/tests/api/endpoints/test_seeds.py +24 -0
  191. mage_ai/tests/api/operations/test_sessions.py +53 -2
  192. mage_ai/tests/authentication/providers/test_okta.py +43 -0
  193. mage_ai/tests/orchestration/db/models/test_oauth.py +3 -3
  194. mage_ai/tests/orchestration/queue/test_process_queue.py +1 -0
  195. mage_ai/tests/server/test_server.py +8 -4
  196. {mage_ai-0.9.68.dist-info → mage_ai-0.9.69.dist-info}/METADATA +4 -4
  197. {mage_ai-0.9.68.dist-info → mage_ai-0.9.69.dist-info}/RECORD +203 -200
  198. mage_ai/server/frontend_dist/_next/static/chunks/1557-01f0843dc6ac4971.js +0 -1
  199. mage_ai/server/frontend_dist/_next/static/chunks/2717-b5f9575799b594d5.js +0 -1
  200. mage_ai/server/frontend_dist/_next/static/chunks/3548-961ff79ca70038c7.js +0 -1
  201. mage_ai/server/frontend_dist/_next/static/chunks/5699-6efc749f2f8ddd20.js +0 -1
  202. mage_ai/server/frontend_dist/_next/static/chunks/7361-18d9d8be96e1ce97.js +0 -1
  203. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-08790743315de36a.js +0 -1
  204. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-13760bb72d823b69.js +0 -1
  205. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-bd0aff5a5ed8888c.js +0 -1
  206. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-d1ee961387c58b7f.js +0 -1
  207. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-ceb06e1616ee9610.js +0 -1
  208. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-8ff16ef9566e911a.js +0 -1
  209. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-74d76300942dcee8.js +0 -1
  210. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-d7a8bc51bb7a1082.js +0 -1
  211. mage_ai/server/frontend_dist/_next/static/chunks/pages/sign-in-f59d34429fe022ee.js +0 -1
  212. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-01f0843dc6ac4971.js +0 -1
  213. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-b5f9575799b594d5.js +0 -1
  214. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-961ff79ca70038c7.js +0 -1
  215. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5699-6efc749f2f8ddd20.js +0 -1
  216. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-18d9d8be96e1ce97.js +0 -1
  217. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-08790743315de36a.js +0 -1
  218. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-13760bb72d823b69.js +0 -1
  219. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-bd0aff5a5ed8888c.js +0 -1
  220. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-d1ee961387c58b7f.js +0 -1
  221. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-ceb06e1616ee9610.js +0 -1
  222. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-8ff16ef9566e911a.js +0 -1
  223. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-74d76300942dcee8.js +0 -1
  224. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-d7a8bc51bb7a1082.js +0 -1
  225. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/sign-in-f59d34429fe022ee.js +0 -1
  226. /mage_ai/server/frontend_dist/_next/static/{i8pymuJDTVHdWjUP1QSh1 → _krrrgup_C-dPOpX36S8I}/_ssgManifest.js +0 -0
  227. /mage_ai/server/frontend_dist_base_path_template/_next/static/{CKCvjsYCf2imD2X8zAOBf → KLL5mirre9d7_ZeEpaw3s}/_ssgManifest.js +0 -0
  228. {mage_ai-0.9.68.dist-info → mage_ai-0.9.69.dist-info}/LICENSE +0 -0
  229. {mage_ai-0.9.68.dist-info → mage_ai-0.9.69.dist-info}/WHEEL +0 -0
  230. {mage_ai-0.9.68.dist-info → mage_ai-0.9.69.dist-info}/entry_points.txt +0 -0
  231. {mage_ai-0.9.68.dist-info → mage_ai-0.9.69.dist-info}/top_level.txt +0 -0
@@ -62,6 +62,8 @@ from mage_ai.data_preparation.shared.utils import get_template_vars
62
62
  from mage_ai.data_preparation.templates.utils import copy_template_directory
63
63
  from mage_ai.data_preparation.variable_manager import VariableManager
64
64
  from mage_ai.orchestration.constants import Entity
65
+ from mage_ai.orchestration.notification.config import NotificationConfig
66
+ from mage_ai.orchestration.notification.sender import NotificationSender
65
67
  from mage_ai.settings.platform import build_repo_path_for_all_projects
66
68
  from mage_ai.settings.platform.constants import project_platform_activated
67
69
  from mage_ai.settings.repo import get_repo_path
@@ -339,7 +341,7 @@ class Pipeline:
339
341
  duplicate_pipeline_dict['name'] = duplicate_pipeline_uuid
340
342
  safe_write(
341
343
  duplicate_pipeline.config_path,
342
- yaml.dump(duplicate_pipeline_dict)
344
+ yaml.dump(duplicate_pipeline_dict),
343
345
  )
344
346
 
345
347
  tags = duplicate_pipeline_dict.get('tags')
@@ -514,7 +516,7 @@ class Pipeline:
514
516
 
515
517
  if not os.path.exists(config_path):
516
518
  raise Exception(f'Pipeline {uuid} does not exist.')
517
- async with aiofiles.open(config_path, mode='r') as f:
519
+ async with aiofiles.open(config_path, mode='r', encoding='utf-8') as f:
518
520
  config = yaml.safe_load(await f.read()) or {}
519
521
 
520
522
  if PipelineType.INTEGRATION == config.get('type'):
@@ -673,6 +675,7 @@ class Pipeline:
673
675
  analyze_outputs: bool = False,
674
676
  build_block_output_stdout: Callable[..., object] = None,
675
677
  global_vars=None,
678
+ retry_config=None,
676
679
  run_sensors: bool = True,
677
680
  run_tests: bool = True,
678
681
  update_status: bool = True,
@@ -689,6 +692,7 @@ class Pipeline:
689
692
  StreamingPipelineExecutor(self).execute(
690
693
  build_block_output_stdout=build_block_output_stdout,
691
694
  global_vars=global_vars,
695
+ retry_config=retry_config,
692
696
  )
693
697
  else:
694
698
  root_blocks = []
@@ -715,7 +719,7 @@ class Pipeline:
715
719
  def get_config_from_yaml(self):
716
720
  if not os.path.exists(self.config_path):
717
721
  raise Exception(f'Pipeline {self.uuid} does not exist in repo_path {self.repo_path}.')
718
- with open(self.config_path) as fp:
722
+ with open(self.config_path, encoding='utf-8') as fp:
719
723
  config = yaml.full_load(fp) or {}
720
724
  return config
721
725
 
@@ -726,6 +730,16 @@ class Pipeline:
726
730
  config = json.load(f)
727
731
  return config
728
732
 
733
+ def get_notification_sender(self):
734
+ return NotificationSender(
735
+ NotificationConfig.load(
736
+ config=merge_dict(
737
+ self.repo_config.notification_config,
738
+ self.notification_config,
739
+ ),
740
+ ),
741
+ )
742
+
729
743
  def load_config_from_yaml(self):
730
744
  catalog = None
731
745
  if os.path.exists(self.catalog_config_path):
@@ -1294,7 +1308,9 @@ class Pipeline:
1294
1308
  file_path = (configuration.get('file_source') or {}).get('path')
1295
1309
  if file_path:
1296
1310
  # Check for block name with period to avoid replacing a directory name
1297
- new_file_path = file_path.replace(f'{block.name}.', f'{name}.')
1311
+ new_file_path = file_path.replace(
1312
+ f'{clean_name(block.name)}.', f'{clean_name(name)}.'
1313
+ )
1298
1314
  configuration['file_source']['path'] = new_file_path
1299
1315
  block_update_payload['configuration'] = configuration
1300
1316
  blocks_to_remove_from_cache.append(block.to_dict())
@@ -2132,7 +2148,7 @@ class Pipeline:
2132
2148
  'Blocks cannot be added or removed when saving content, please try again.',
2133
2149
  )
2134
2150
 
2135
- content = yaml.dump(pipeline_dict)
2151
+ content = yaml.dump(pipeline_dict, allow_unicode=True)
2136
2152
 
2137
2153
  safe_write(self.config_path, content)
2138
2154
 
@@ -2205,7 +2221,7 @@ class Pipeline:
2205
2221
  'Blocks cannot be added or removed when saving content, please try again.',
2206
2222
  )
2207
2223
 
2208
- content = yaml.dump(pipeline_dict)
2224
+ content = yaml.dump(pipeline_dict, allow_unicode=True)
2209
2225
 
2210
2226
  test_path = f'{self.config_path}.test'
2211
2227
  async with aiofiles.open(test_path, mode='w', encoding='utf-8') as fp:
@@ -19,6 +19,12 @@ STRING_SERIALIZABLE_COLUMN_TYPES = set([
19
19
  'ObjectId',
20
20
  ])
21
21
 
22
+ AMBIGUOUS_COLUMN_TYPES = set([
23
+ 'mixed-integer',
24
+ 'complex',
25
+ 'unknown-array',
26
+ ])
27
+
22
28
  CAST_TYPE_COLUMN_TYPES = set([
23
29
  'Int64',
24
30
  'int64',
@@ -7,7 +7,7 @@ from typing import Any, Dict, List
7
7
  import numpy as np
8
8
  import pandas as pd
9
9
  import polars as pl
10
- from pandas.api.types import is_object_dtype
10
+ from pandas.api.types import infer_dtype, is_object_dtype
11
11
  from pandas.core.indexes.range import RangeIndex
12
12
 
13
13
  from mage_ai.data_cleaner.shared.utils import is_geo_dataframe, is_spark_dataframe
@@ -18,6 +18,7 @@ from mage_ai.data_preparation.models.constants import (
18
18
  VARIABLE_DIR,
19
19
  )
20
20
  from mage_ai.data_preparation.models.utils import ( # dask_from_pandas,
21
+ AMBIGUOUS_COLUMN_TYPES,
21
22
  STRING_SERIALIZABLE_COLUMN_TYPES,
22
23
  apply_transform_pandas,
23
24
  cast_column_types,
@@ -282,7 +283,9 @@ class Variable:
282
283
  else:
283
284
  self.__write_json(data)
284
285
 
285
- self.write_metadata()
286
+ if self.variable_type != VariableType.SPARK_DATAFRAME:
287
+ # Not write json file in spark data directory to avoid read error
288
+ self.write_metadata()
286
289
 
287
290
  async def write_data_async(self, data: Any) -> None:
288
291
  """
@@ -315,7 +318,9 @@ class Variable:
315
318
  else:
316
319
  await self.__write_json_async(data)
317
320
 
318
- self.write_metadata()
321
+ if self.variable_type != VariableType.SPARK_DATAFRAME:
322
+ # Not write json file in spark data directory to avoid read error
323
+ self.write_metadata()
319
324
 
320
325
  def write_metadata(self) -> None:
321
326
  """
@@ -578,10 +583,19 @@ class Variable:
578
583
  else:
579
584
  series_non_null = df_col.dropna()
580
585
  if len(series_non_null) > 0:
581
- coltype = type(series_non_null.iloc[0])
586
+ sample_element = series_non_null.iloc[0]
587
+ coltype = type(sample_element)
588
+ coltype_inferred = infer_dtype(series_non_null)
582
589
  if is_object_dtype(series_non_null.dtype):
583
590
  if coltype.__name__ in STRING_SERIALIZABLE_COLUMN_TYPES:
584
591
  cast_coltype = str
592
+ # If the column is a "primitive" type, i.e. int/bool/etc and there is
593
+ # a mix of types in the column, cast to string
594
+ elif (
595
+ not hasattr(sample_element, '__dict__')
596
+ and coltype_inferred in AMBIGUOUS_COLUMN_TYPES
597
+ ):
598
+ cast_coltype = str
585
599
  else:
586
600
  cast_coltype = coltype
587
601
  try:
@@ -310,8 +310,9 @@ def get_cluster_type(repo_path=None) -> Optional[ClusterType]:
310
310
 
311
311
  def set_project_uuid_from_metadata() -> None:
312
312
  global project_uuid
313
- if os.path.exists(get_metadata_path()):
314
- with open(get_metadata_path(), 'r', encoding='utf-8') as f:
313
+ metadata_path = get_metadata_path(root_project=True)
314
+ if os.path.exists(metadata_path):
315
+ with open(metadata_path, 'r', encoding='utf-8') as f:
315
316
  config = yml.load(f) or {}
316
317
  project_uuid = config.get('project_uuid')
317
318
 
@@ -25,7 +25,7 @@ def get_template_vars_no_db(include_python_libraries: Dict = None) -> Dict[str,
25
25
  try:
26
26
  from mage_ai.services.aws.secrets_manager.secrets_manager import get_secret
27
27
  kwargs['aws_secret_var'] = get_secret
28
- except ImportError:
28
+ except Exception:
29
29
  pass
30
30
 
31
31
  try:
@@ -23,8 +23,8 @@ def export_data_to_mysql(df: DataFrame, **kwargs) -> None:
23
23
  with MySQL.with_config(ConfigFileLoader(config_path, config_profile)) as loader:
24
24
  loader.export(
25
25
  df,
26
- None,
27
- table_name,
26
+ schema_name=None,
27
+ table_name=table_name,
28
28
  index=False, # Specifies whether to include index in exported table
29
29
  if_exists='replace', # Specify resolution policy if table name already exists
30
30
  )
@@ -0,0 +1,27 @@
1
+ from os import path
2
+
3
+ from pandas import DataFrame
4
+
5
+ from mage_ai.settings.repo import get_repo_path
6
+ from mage_ai.io.config import ConfigFileLoader
7
+ from mage_ai.io.oracledb import OracleDB
8
+
9
+ if 'data_exporter' not in globals():
10
+ from mage_ai.data_preparation.decorators import data_exporter
11
+
12
+
13
+ @data_exporter
14
+ def export_data_to_oracledb(df: DataFrame, **kwargs) -> None:
15
+ """
16
+ Template to export data to Oracledb.
17
+ """
18
+ config_path = path.join(get_repo_path(), 'io_config.yaml')
19
+ config_profile = 'default'
20
+ table_name = 'your_table_name'
21
+
22
+ with OracleDB.with_config(ConfigFileLoader(config_path, config_profile)) as exporter:
23
+ exporter.export(
24
+ df,
25
+ table_name=table_name,
26
+ if_exists='replace', # Specify resolution policy if table name already exists
27
+ )
@@ -43,6 +43,7 @@ spark_config:
43
43
  # e.g. kwargs['context']['spark'] = spark_session
44
44
  custom_session_var_name: 'spark'
45
45
 
46
+ help_improve_mage: true
46
47
  notification_config:
47
48
  alert_on:
48
49
  - trigger_failure
mage_ai/io/bigquery.py CHANGED
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from typing import Dict, List, Mapping, Union
2
3
 
3
4
  import numpy as np
@@ -14,6 +15,7 @@ from sqlglot import exp, parse_one
14
15
 
15
16
  from mage_ai.io.base import QUERY_ROW_LIMIT, BaseSQLDatabase, ExportWritePolicy
16
17
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
18
+ from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
17
19
  from mage_ai.io.export_utils import infer_dtypes
18
20
  from mage_ai.shared.custom_logger import DX_PRINTER
19
21
  from mage_ai.shared.environments import is_debug
@@ -200,6 +202,9 @@ WHERE TABLE_NAME = '{table_name}'
200
202
  overwrite_types: Dict = None,
201
203
  query_string: Union[str, None] = None,
202
204
  verbose: bool = True,
205
+ unique_conflict_method: str = None,
206
+ unique_constraints: List[str] = None,
207
+ write_disposition: str = None,
203
208
  **configuration_params,
204
209
  ) -> None:
205
210
  """
@@ -230,24 +235,23 @@ WHERE TABLE_NAME = '{table_name}'
230
235
  elif type(df) is list:
231
236
  df = DataFrame(df)
232
237
 
233
- def __process(database: Union[str, None]):
234
- if query_string:
235
- parts = table_id.split('.')
236
- if len(parts) == 2:
237
- schema, table_name = parts
238
- elif len(parts) == 3:
239
- database, schema, table_name = parts
238
+ def __process(database: Union[str, None], write_disposition: str = None):
239
+ parts = table_id.split('.')
240
+ if len(parts) == 2:
241
+ schema, table_name = parts
242
+ elif len(parts) == 3:
243
+ database, schema, table_name = parts
240
244
 
241
- df_existing = self.client.query(f"""
245
+ df_existing = self.client.query(f"""
242
246
  SELECT 1
243
247
  FROM `{database}.{schema}.__TABLES_SUMMARY__`
244
248
  WHERE table_id = '{table_name}'
245
249
  """).to_dataframe()
246
250
 
247
- full_table_name = f'`{database}.{schema}.{table_name}`'
248
-
249
- table_doesnt_exist = df_existing.empty
251
+ full_table_name = f'`{database}.{schema}.{table_name}`'
250
252
 
253
+ table_doesnt_exist = df_existing.empty
254
+ if query_string:
251
255
  if ExportWritePolicy.FAIL == if_exists and not table_doesnt_exist:
252
256
  raise ValueError(
253
257
  f'Table \'{full_table_name}\' already exists in database.',
@@ -268,60 +272,129 @@ WHERE table_id = '{table_name}'
268
272
  self.client.query(sql)
269
273
 
270
274
  else:
271
- config = LoadJobConfig(**configuration_params)
272
- if overwrite_types is not None:
273
- config.schema = [SchemaField(k, v) for k, v in overwrite_types.items()]
274
- if 'write_disposition' not in configuration_params:
275
- if if_exists == 'replace':
276
- config.write_disposition = WriteDisposition.WRITE_TRUNCATE
277
- elif if_exists == 'append':
278
- config.write_disposition = WriteDisposition.WRITE_APPEND
279
- elif if_exists == 'fail':
280
- config.write_disposition = WriteDisposition.WRITE_EMPTY
281
- else:
282
- raise ValueError(
283
- f'Invalid policy specified for handling existence of '
284
- f'table: \'{if_exists}\''
275
+ if (
276
+ if_exists == ExportWritePolicy.APPEND
277
+ and not table_doesnt_exist
278
+ and unique_constraints
279
+ and unique_conflict_method
280
+ ):
281
+ temp_table_id = f'{table_id}_{uuid.uuid4().hex}'
282
+
283
+ try:
284
+ self.__write_table(
285
+ df,
286
+ temp_table_id,
287
+ overwrite_types=overwrite_types,
288
+ **configuration_params,
289
+ )
290
+
291
+ parts = temp_table_id.split('.')
292
+ if len(parts) == 2:
293
+ temp_table_name = parts[1]
294
+ elif len(parts) == 3:
295
+ temp_table_name = parts[2]
296
+ column_types = self.get_column_types(schema, temp_table_name)
297
+ columns = list(column_types.keys())
298
+ if not columns:
299
+ columns = df.columns.str.replace(' ', '_')
300
+
301
+ on_conditions = []
302
+ for col in unique_constraints:
303
+ on_conditions.append(
304
+ f'((a.{col} IS NULL AND b.{col} IS NULL) OR a.{col} = b.{col})',
305
+ )
306
+
307
+ insert_columns = ', '.join([f'`{col}`' for col in columns])
308
+
309
+ merge_commands = [
310
+ f'MERGE INTO `{table_id}` AS a',
311
+ f'USING (SELECT * FROM `{temp_table_id}`) AS b',
312
+ f"ON {' AND '.join(on_conditions)}",
313
+ ]
314
+
315
+ if UNIQUE_CONFLICT_METHOD_UPDATE == unique_conflict_method:
316
+ set_command = ', '.join(
317
+ [f'a.`{col}` = b.`{col}`' for col in columns],
318
+ )
319
+ merge_commands.append(f'WHEN MATCHED THEN UPDATE SET {set_command}')
320
+
321
+ merge_values = f"({', '.join([f'b.`{col}`' for col in columns])})"
322
+ merge_commands.append(
323
+ f'WHEN NOT MATCHED THEN INSERT ({insert_columns}) VALUES {merge_values}', # noqa: E501
285
324
  )
286
- parts = table_id.split('.')
287
- if len(parts) == 2:
288
- schema = parts[0]
289
- table_name = parts[1]
290
- elif len(parts) == 3:
291
- schema = parts[1]
292
- table_name = parts[2]
293
-
294
- self.client.create_dataset(dataset=schema, exists_ok=True)
295
-
296
- column_types = self.get_column_types(schema, table_name)
297
-
298
- if df is not None:
299
- df.fillna(value=np.NaN, inplace=True)
300
- for col in df.columns:
301
- col_type = column_types.get(col)
302
- if not col_type:
303
- continue
304
-
305
- null_rows = df[col].isnull()
306
- if col_type.startswith('ARRAY<STRUCT'):
307
- df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [{}])
308
- elif col_type.startswith('ARRAY'):
309
- df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [])
310
- elif col_type.startswith('STRUCT'):
311
- df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: {})
312
-
313
- # Clean column names
314
- if type(df) is DataFrame:
315
- df.columns = df.columns.str.replace(' ', '_')
316
-
317
- self.client.load_table_from_dataframe(df, table_id, job_config=config).result()
325
+
326
+ merge_command = '\n'.join(merge_commands)
327
+
328
+ self.client.query(merge_command).result()
329
+ finally:
330
+ self.client.query(f'DROP TABLE IF EXISTS {temp_table_id}').result()
331
+ else:
332
+ if not write_disposition:
333
+ if if_exists == ExportWritePolicy.APPEND:
334
+ write_disposition = WriteDisposition.WRITE_APPEND
335
+ elif if_exists == ExportWritePolicy.REPLACE:
336
+ write_disposition = WriteDisposition.WRITE_TRUNCATE
337
+ elif if_exists == ExportWritePolicy.FAIL:
338
+ write_disposition = WriteDisposition.WRITE_EMPTY
339
+ self.__write_table(
340
+ df,
341
+ table_id,
342
+ overwrite_types=overwrite_types,
343
+ write_disposition=write_disposition,
344
+ **configuration_params,
345
+ )
318
346
 
319
347
  if verbose:
320
348
  with self.printer.print_msg(f'Exporting data to table \'{table_id}\''):
321
- __process(database=database)
349
+ __process(database=database, write_disposition=write_disposition)
322
350
  else:
323
351
  __process(database=database)
324
352
 
353
+ def __write_table(
354
+ self,
355
+ df: DataFrame,
356
+ table_id: str,
357
+ overwrite_types: Dict = None,
358
+ **configuration_params,
359
+ ):
360
+ config = LoadJobConfig(**configuration_params)
361
+ if overwrite_types is not None:
362
+ config.schema = [SchemaField(k, v) for k, v in overwrite_types.items()]
363
+ if not config.write_disposition:
364
+ config.write_disposition = WriteDisposition.WRITE_APPEND
365
+ parts = table_id.split('.')
366
+ if len(parts) == 2:
367
+ schema = parts[0]
368
+ table_name = parts[1]
369
+ elif len(parts) == 3:
370
+ schema = parts[1]
371
+ table_name = parts[2]
372
+
373
+ self.client.create_dataset(dataset=schema, exists_ok=True)
374
+
375
+ column_types = self.get_column_types(schema, table_name)
376
+
377
+ if df is not None:
378
+ df.fillna(value=np.NaN, inplace=True)
379
+ for col in df.columns:
380
+ col_type = column_types.get(col)
381
+ if not col_type:
382
+ continue
383
+
384
+ null_rows = df[col].isnull()
385
+ if col_type.startswith('ARRAY<STRUCT'):
386
+ df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [{}])
387
+ elif col_type.startswith('ARRAY'):
388
+ df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [])
389
+ elif col_type.startswith('STRUCT'):
390
+ df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: {})
391
+
392
+ # Clean column names
393
+ if type(df) is DataFrame:
394
+ df.columns = df.columns.str.replace(' ', '_')
395
+
396
+ return self.client.load_table_from_dataframe(df, table_id, job_config=config).result()
397
+
325
398
  def execute(self, query_string: str, **kwargs) -> None:
326
399
  """
327
400
  Sends query to the connected BigQuery warehouse.
mage_ai/io/mysql.py CHANGED
@@ -8,6 +8,7 @@ from mysql.connector.cursor import MySQLCursor
8
8
  from pandas import DataFrame, Series
9
9
 
10
10
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
11
+ from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
11
12
  from mage_ai.io.export_utils import PandasTypes
12
13
  from mage_ai.io.sql import BaseSQL
13
14
  from mage_ai.shared.parsers import encode_complex
@@ -60,7 +61,7 @@ class MySQL(BaseSQL):
60
61
  ) -> str:
61
62
  if unique_constraints is None:
62
63
  unique_constraints = []
63
- query = []
64
+ columns_and_types = []
64
65
  for cname in dtypes:
65
66
  if overwrite_types is not None and cname in overwrite_types.keys():
66
67
  dtypes[cname] = overwrite_types[cname]
@@ -68,9 +69,22 @@ class MySQL(BaseSQL):
68
69
  cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
69
70
  else:
70
71
  cleaned_col_name = cname
71
- query.append(f'`{cleaned_col_name}` {dtypes[cname]} NULL')
72
-
73
- return f'CREATE TABLE {table_name} (' + ','.join(query) + ');'
72
+ columns_and_types.append(f'`{cleaned_col_name}` {dtypes[cname]} NULL')
73
+
74
+ if unique_constraints:
75
+ unique_constraints = [
76
+ clean_name(col, case_sensitive=case_sensitive)
77
+ for col in unique_constraints
78
+ ]
79
+ index_name = '_'.join([
80
+ clean_name(table_name, case_sensitive=case_sensitive),
81
+ ] + unique_constraints)
82
+ index_name = f'unique{index_name}'[:64]
83
+ columns_and_types.append(
84
+ f"CONSTRAINT {index_name} Unique({', '.join(unique_constraints)})"
85
+ )
86
+
87
+ return f'CREATE TABLE {table_name} (' + ','.join(columns_and_types) + ');'
74
88
 
75
89
  def open(self) -> None:
76
90
  with self.printer.print_msg('Opening connection to MySQL database'):
@@ -94,6 +108,9 @@ class MySQL(BaseSQL):
94
108
  dtypes: List[str],
95
109
  full_table_name: str,
96
110
  buffer: Union[IO, None] = None,
111
+ case_sensitive: bool = False,
112
+ unique_constraints: List[str] = None,
113
+ unique_conflict_method: str = None,
97
114
  **kwargs,
98
115
  ) -> None:
99
116
  def serialize_obj(val):
@@ -133,9 +150,24 @@ class MySQL(BaseSQL):
133
150
  for _, row in df_.iterrows():
134
151
  values.append(tuple([str(val) if type(val) is pd.Timestamp else val for val in row]))
135
152
 
136
- insert_columns = ', '.join([f'`{col}`'for col in columns])
153
+ cleaned_columns = [clean_name(col, case_sensitive=case_sensitive) for col in columns]
154
+ insert_columns = ', '.join([f'`{col}`'for col in cleaned_columns])
155
+
156
+ query = [
157
+ f'INSERT INTO {full_table_name} ({insert_columns})',
158
+ f'VALUES ({values_placeholder})',
159
+ ]
160
+
161
+ if unique_constraints and unique_conflict_method:
162
+ if UNIQUE_CONFLICT_METHOD_UPDATE == unique_conflict_method:
163
+ update_command = [f'{col} = new.{col}' for col in cleaned_columns]
164
+ query += [
165
+ 'AS new',
166
+ f"ON DUPLICATE KEY UPDATE {', '.join(update_command)}",
167
+ ]
168
+
169
+ sql = '\n'.join(query)
137
170
 
138
- sql = f'INSERT INTO {full_table_name} ({insert_columns}) VALUES ({values_placeholder})'
139
171
  cursor.executemany(sql, values)
140
172
 
141
173
  def get_type(self, column: Series, dtype: str) -> str: