mage-ai 0.9.67__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 (307) hide show
  1. mage_ai/api/monitors/BaseMonitor.py +1 -2
  2. mage_ai/api/policies/PipelinePolicy.py +2 -0
  3. mage_ai/api/resources/BlockLayoutItemResource.py +2 -2
  4. mage_ai/api/resources/BlockResource.py +4 -3
  5. mage_ai/api/resources/CacheItemResource.py +1 -1
  6. mage_ai/api/resources/GitBranchResource.py +3 -5
  7. mage_ai/api/resources/PageBlockLayoutResource.py +2 -2
  8. mage_ai/api/resources/PipelineResource.py +13 -0
  9. mage_ai/api/resources/PipelineRunResource.py +10 -1
  10. mage_ai/api/resources/SeedResource.py +2 -1
  11. mage_ai/api/resources/SessionResource.py +13 -1
  12. mage_ai/authentication/permissions/constants.py +2 -0
  13. mage_ai/authentication/permissions/seed.py +32 -21
  14. mage_ai/authentication/providers/active_directory.py +4 -3
  15. mage_ai/authentication/providers/okta.py +22 -83
  16. mage_ai/cache/tag.py +3 -0
  17. mage_ai/cluster_manager/manage.py +4 -1
  18. mage_ai/data_preparation/executors/k8s_block_executor.py +8 -1
  19. mage_ai/data_preparation/executors/k8s_pipeline_executor.py +12 -1
  20. mage_ai/data_preparation/executors/streaming_pipeline_executor.py +77 -7
  21. mage_ai/data_preparation/logging/gcs_logger_manager.py +7 -4
  22. mage_ai/data_preparation/models/block/__init__.py +28 -71
  23. mage_ai/data_preparation/models/block/block_factory.py +77 -0
  24. mage_ai/data_preparation/models/block/data_integration/mixins.py +16 -5
  25. mage_ai/data_preparation/models/block/dbt/block.py +5 -7
  26. mage_ai/data_preparation/models/block/dynamic/child.py +3 -0
  27. mage_ai/data_preparation/models/block/dynamic/variables.py +2 -2
  28. mage_ai/data_preparation/models/block/extension/utils.py +1 -0
  29. mage_ai/data_preparation/models/block/global_data_product/__init__.py +9 -3
  30. mage_ai/data_preparation/models/block/integration/__init__.py +13 -9
  31. mage_ai/data_preparation/models/block/platform/mixins.py +1 -1
  32. mage_ai/data_preparation/models/block/sql/__init__.py +1 -1
  33. mage_ai/data_preparation/models/pipeline.py +102 -19
  34. mage_ai/data_preparation/models/utils.py +6 -0
  35. mage_ai/data_preparation/models/variable.py +18 -4
  36. mage_ai/data_preparation/repo_manager.py +3 -2
  37. mage_ai/data_preparation/shared/utils.py +1 -1
  38. mage_ai/data_preparation/storage/gcs_storage.py +1 -1
  39. mage_ai/data_preparation/templates/constants.py +7 -0
  40. mage_ai/data_preparation/templates/data_exporters/mysql.py +2 -2
  41. mage_ai/data_preparation/templates/data_exporters/oracledb.py +27 -0
  42. mage_ai/data_preparation/templates/repo/metadata.yaml +1 -0
  43. mage_ai/io/bigquery.py +131 -58
  44. mage_ai/io/export_utils.py +3 -0
  45. mage_ai/io/mysql.py +38 -6
  46. mage_ai/io/oracledb.py +138 -3
  47. mage_ai/io/snowflake.py +152 -29
  48. mage_ai/io/sql.py +4 -0
  49. mage_ai/orchestration/db/__init__.py +2 -2
  50. mage_ai/orchestration/db/models/oauth.py +4 -4
  51. mage_ai/orchestration/db/models/schedules.py +10 -3
  52. mage_ai/orchestration/job_manager.py +6 -0
  53. mage_ai/orchestration/notification/sender.py +8 -0
  54. mage_ai/orchestration/pipeline_scheduler_original.py +26 -7
  55. mage_ai/orchestration/queue/celery_queue.py +8 -1
  56. mage_ai/orchestration/queue/process_queue.py +67 -4
  57. mage_ai/orchestration/queue/queue.py +8 -0
  58. mage_ai/server/constants.py +1 -1
  59. mage_ai/server/frontend_dist/404.html +2 -2
  60. mage_ai/server/{frontend_dist_base_path_template/_next/static/khKiaJtwrslgMmp4YSa1f → frontend_dist/_next/static/_krrrgup_C-dPOpX36S8I}/_buildManifest.js +1 -1
  61. mage_ai/server/frontend_dist/_next/static/chunks/1557-df144fbd8b2208c3.js +1 -0
  62. mage_ai/server/frontend_dist/_next/static/chunks/2717-d9200be634dd6766.js +1 -0
  63. mage_ai/server/frontend_dist/_next/static/chunks/3548-fa0792ddb88f4646.js +1 -0
  64. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/4241-ccd0126f5750cc35.js → frontend_dist/_next/static/chunks/4241-4499461184ba0d23.js} +1 -1
  65. mage_ai/server/frontend_dist/_next/static/chunks/5627-237a3de578538022.js +1 -0
  66. mage_ai/server/frontend_dist/_next/static/chunks/5699-6d708c6b2153ea08.js +1 -0
  67. mage_ai/server/frontend_dist/_next/static/chunks/7361-8a23dd8360593e7a.js +1 -0
  68. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/7966-5c1786fb7e7a48f5.js → frontend_dist/_next/static/chunks/7966-f07b2913f7326b50.js} +1 -1
  69. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-d9c89527266296f7.js +1 -0
  70. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-4e12783b064c1cfe.js +1 -0
  71. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/[user]-8bbfa0c19b5e4cb3.js +1 -0
  72. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-852d403c7bda21b3.js +1 -0
  73. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-597b74828bf105db.js +1 -0
  74. mage_ai/server/frontend_dist/_next/static/chunks/pages/{pipeline-runs-a66b4c7641ae03eb.js → pipeline-runs-3edc6270c5b0e962.js} +1 -1
  75. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-a8b61d8d239fd16f.js +1 -0
  76. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-e1dd1ed71d26c10d.js +1 -0
  77. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-1417ad1c821d720a.js +1 -0
  78. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-59aca25a5b1d3998.js +1 -0
  79. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/syncs-90abafc7ed61c582.js +1 -0
  80. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers-1bdfda8edc9cf4a8.js +1 -0
  81. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-3591d035bb3bb2b8.js +1 -0
  82. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-503049734a8b082f.js +1 -0
  83. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-c2e9ef989c8bfa73.js +1 -0
  84. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-5b26eeda8aed8a7b.js +1 -0
  85. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/sync-data-79a4cf66a623e667.js → frontend_dist/_next/static/chunks/pages/settings/workspace/sync-data-8b793b3b696a2cd3.js} +1 -1
  86. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{users-86814e581acaf5db.js → users-a4db8710f703c729.js} +1 -1
  87. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/sign-in-7d38b2f7c3e918a1.js → frontend_dist/_next/static/chunks/pages/sign-in-09414a8b66fb6f06.js} +1 -1
  88. mage_ai/server/frontend_dist/_next/static/chunks/pages/triggers-9cba3211434a8966.js +1 -0
  89. mage_ai/server/frontend_dist/block-layout.html +2 -2
  90. mage_ai/server/frontend_dist/compute.html +2 -2
  91. mage_ai/server/frontend_dist/files.html +2 -2
  92. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  93. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  94. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  95. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  96. mage_ai/server/frontend_dist/index.html +2 -2
  97. mage_ai/server/frontend_dist/manage/files.html +2 -2
  98. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  99. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  100. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  101. mage_ai/server/frontend_dist/manage/users.html +2 -2
  102. mage_ai/server/frontend_dist/manage.html +2 -2
  103. mage_ai/server/frontend_dist/oauth.html +3 -3
  104. mage_ai/server/frontend_dist/overview.html +2 -2
  105. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  106. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  107. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  108. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  109. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  110. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  111. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  112. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  113. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  114. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  115. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  116. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  117. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  118. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  119. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  120. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  121. mage_ai/server/frontend_dist/pipelines.html +2 -2
  122. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
  123. mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
  124. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  125. mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
  126. mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
  127. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  128. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  129. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  130. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  131. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  132. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  133. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  134. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  135. mage_ai/server/frontend_dist/settings.html +2 -2
  136. mage_ai/server/frontend_dist/sign-in.html +5 -5
  137. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  138. mage_ai/server/frontend_dist/templates.html +2 -2
  139. mage_ai/server/frontend_dist/terminal.html +2 -2
  140. mage_ai/server/frontend_dist/test.html +2 -2
  141. mage_ai/server/frontend_dist/triggers.html +2 -2
  142. mage_ai/server/frontend_dist/version-control.html +2 -2
  143. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  144. mage_ai/server/{frontend_dist/_next/static/vPsMu6Fi2zrHaf2fRXKRO → frontend_dist_base_path_template/_next/static/KLL5mirre9d7_ZeEpaw3s}/_buildManifest.js +1 -1
  145. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-df144fbd8b2208c3.js +1 -0
  146. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-d9200be634dd6766.js +1 -0
  147. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-fa0792ddb88f4646.js +1 -0
  148. mage_ai/server/{frontend_dist/_next/static/chunks/4241-ccd0126f5750cc35.js → frontend_dist_base_path_template/_next/static/chunks/4241-4499461184ba0d23.js} +1 -1
  149. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5627-237a3de578538022.js +1 -0
  150. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5699-6d708c6b2153ea08.js +1 -0
  151. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-8a23dd8360593e7a.js +1 -0
  152. mage_ai/server/{frontend_dist/_next/static/chunks/7966-5c1786fb7e7a48f5.js → frontend_dist_base_path_template/_next/static/chunks/7966-f07b2913f7326b50.js} +1 -1
  153. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-d9c89527266296f7.js +1 -0
  154. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-4e12783b064c1cfe.js +1 -0
  155. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/[user]-8bbfa0c19b5e4cb3.js +1 -0
  156. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage-852d403c7bda21b3.js +1 -0
  157. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-597b74828bf105db.js +1 -0
  158. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{pipeline-runs-a66b4c7641ae03eb.js → pipeline-runs-3edc6270c5b0e962.js} +1 -1
  159. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-a8b61d8d239fd16f.js +1 -0
  160. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-e1dd1ed71d26c10d.js +1 -0
  161. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-1417ad1c821d720a.js +1 -0
  162. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-59aca25a5b1d3998.js +1 -0
  163. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/syncs-90abafc7ed61c582.js +1 -0
  164. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers-1bdfda8edc9cf4a8.js +1 -0
  165. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-3591d035bb3bb2b8.js +1 -0
  166. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-503049734a8b082f.js +1 -0
  167. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-c2e9ef989c8bfa73.js +1 -0
  168. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-5b26eeda8aed8a7b.js +1 -0
  169. mage_ai/server/{frontend_dist/_next/static/chunks/pages/settings/workspace/sync-data-79a4cf66a623e667.js → frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/sync-data-8b793b3b696a2cd3.js} +1 -1
  170. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{users-86814e581acaf5db.js → users-a4db8710f703c729.js} +1 -1
  171. mage_ai/server/{frontend_dist/_next/static/chunks/pages/sign-in-7d38b2f7c3e918a1.js → frontend_dist_base_path_template/_next/static/chunks/pages/sign-in-09414a8b66fb6f06.js} +1 -1
  172. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/triggers-9cba3211434a8966.js +1 -0
  173. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  174. mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
  175. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  176. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  177. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  178. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
  179. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
  180. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  181. mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
  182. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  183. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  184. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  185. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  186. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  187. mage_ai/server/frontend_dist_base_path_template/oauth.html +3 -3
  188. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  189. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  190. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  191. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  192. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
  193. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  194. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  195. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  196. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  197. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  198. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  199. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  200. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  201. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  202. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  203. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  204. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  205. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  206. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
  207. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
  208. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  209. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
  210. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
  211. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
  212. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
  213. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  214. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
  215. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
  216. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  217. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
  218. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  219. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  220. mage_ai/server/frontend_dist_base_path_template/sign-in.html +5 -5
  221. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  222. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  223. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  224. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  225. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  226. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  227. mage_ai/server/scheduler_manager.py +7 -0
  228. mage_ai/server/server.py +12 -5
  229. mage_ai/server/websocket_server.py +1 -0
  230. mage_ai/services/k8s/job_manager.py +8 -0
  231. mage_ai/services/slack/slack.py +10 -1
  232. mage_ai/services/spark/spark.py +9 -2
  233. mage_ai/settings/backends.py +8 -8
  234. mage_ai/settings/keys/auth.py +2 -0
  235. mage_ai/settings/models/configuration_option.py +10 -9
  236. mage_ai/settings/server.py +1 -1
  237. mage_ai/shared/files.py +19 -1
  238. mage_ai/shared/path_fixer.py +3 -0
  239. mage_ai/streaming/sources/base.py +5 -0
  240. mage_ai/streaming/sources/influxdb.py +2 -0
  241. mage_ai/streaming/sources/kafka.py +2 -1
  242. mage_ai/streaming/sources/mongodb.py +4 -0
  243. mage_ai/tests/api/endpoints/mixins.py +10 -9
  244. mage_ai/tests/api/endpoints/test_seeds.py +24 -0
  245. mage_ai/tests/api/operations/test_sessions.py +53 -2
  246. mage_ai/tests/authentication/providers/test_okta.py +43 -0
  247. mage_ai/tests/data_preparation/models/block/dbt/test_block.py +2 -2
  248. mage_ai/tests/data_preparation/models/block/dbt/test_block_sql.py +1 -1
  249. mage_ai/tests/data_preparation/models/block/dbt/test_block_yaml.py +1 -1
  250. mage_ai/tests/data_preparation/models/test_block.py +2 -1
  251. mage_ai/tests/orchestration/db/models/test_oauth.py +3 -3
  252. mage_ai/tests/orchestration/queue/test_process_queue.py +1 -0
  253. mage_ai/tests/orchestration/test_pipeline_scheduler.py +2 -0
  254. mage_ai/tests/server/test_server.py +8 -4
  255. mage_ai/tests/settings/models/test_configuration_option.py +2 -2
  256. {mage_ai-0.9.67.dist-info → mage_ai-0.9.69.dist-info}/METADATA +6 -6
  257. {mage_ai-0.9.67.dist-info → mage_ai-0.9.69.dist-info}/RECORD +263 -259
  258. mage_ai/server/frontend_dist/_next/static/chunks/1557-01f0843dc6ac4971.js +0 -1
  259. mage_ai/server/frontend_dist/_next/static/chunks/181-e61915415a976861.js +0 -1
  260. mage_ai/server/frontend_dist/_next/static/chunks/2717-b5f9575799b594d5.js +0 -1
  261. mage_ai/server/frontend_dist/_next/static/chunks/3548-13563a1ff815f922.js +0 -1
  262. mage_ai/server/frontend_dist/_next/static/chunks/5699-6efc749f2f8ddd20.js +0 -1
  263. mage_ai/server/frontend_dist/_next/static/chunks/7361-18d9d8be96e1ce97.js +0 -1
  264. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-1c1ffd928f5a00f7.js +0 -1
  265. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-b7b8695a7f9efde2.js +0 -1
  266. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/[user]-d3a5fd3119fdb1e4.js +0 -1
  267. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-42789d698d28a92f.js +0 -1
  268. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-d05040edba41b2ac.js +0 -1
  269. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-aaf393c86fc1bda3.js +0 -1
  270. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-36377e679da2cd91.js +0 -1
  271. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-1b688d61f8efe07a.js +0 -1
  272. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-ed3331d22d5cff7d.js +0 -1
  273. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/syncs-d94e48bad89ba1e0.js +0 -1
  274. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers-f508c2f261297724.js +0 -1
  275. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-f99e99aa8f45529c.js +0 -1
  276. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-6826000cdffc36b8.js +0 -1
  277. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-74d76300942dcee8.js +0 -1
  278. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-dde29a463495cebb.js +0 -1
  279. mage_ai/server/frontend_dist/_next/static/chunks/pages/triggers-ab98a7b3a678669e.js +0 -1
  280. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-01f0843dc6ac4971.js +0 -1
  281. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/181-e61915415a976861.js +0 -1
  282. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-b5f9575799b594d5.js +0 -1
  283. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-13563a1ff815f922.js +0 -1
  284. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5699-6efc749f2f8ddd20.js +0 -1
  285. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-18d9d8be96e1ce97.js +0 -1
  286. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-1c1ffd928f5a00f7.js +0 -1
  287. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-b7b8695a7f9efde2.js +0 -1
  288. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/[user]-d3a5fd3119fdb1e4.js +0 -1
  289. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage-42789d698d28a92f.js +0 -1
  290. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-d05040edba41b2ac.js +0 -1
  291. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-aaf393c86fc1bda3.js +0 -1
  292. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-36377e679da2cd91.js +0 -1
  293. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-1b688d61f8efe07a.js +0 -1
  294. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-ed3331d22d5cff7d.js +0 -1
  295. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/syncs-d94e48bad89ba1e0.js +0 -1
  296. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers-f508c2f261297724.js +0 -1
  297. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-f99e99aa8f45529c.js +0 -1
  298. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-6826000cdffc36b8.js +0 -1
  299. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-74d76300942dcee8.js +0 -1
  300. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-dde29a463495cebb.js +0 -1
  301. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/triggers-ab98a7b3a678669e.js +0 -1
  302. /mage_ai/server/frontend_dist/_next/static/{vPsMu6Fi2zrHaf2fRXKRO → _krrrgup_C-dPOpX36S8I}/_ssgManifest.js +0 -0
  303. /mage_ai/server/frontend_dist_base_path_template/_next/static/{khKiaJtwrslgMmp4YSa1f → KLL5mirre9d7_ZeEpaw3s}/_ssgManifest.js +0 -0
  304. {mage_ai-0.9.67.dist-info → mage_ai-0.9.69.dist-info}/LICENSE +0 -0
  305. {mage_ai-0.9.67.dist-info → mage_ai-0.9.69.dist-info}/WHEEL +0 -0
  306. {mage_ai-0.9.67.dist-info → mage_ai-0.9.69.dist-info}/entry_points.txt +0 -0
  307. {mage_ai-0.9.67.dist-info → mage_ai-0.9.69.dist-info}/top_level.txt +0 -0
mage_ai/io/snowflake.py CHANGED
@@ -11,6 +11,7 @@ from mage_ai.data_preparation.models.block.sql.utils.shared import (
11
11
  )
12
12
  from mage_ai.io.base import QUERY_ROW_LIMIT, BaseSQLConnection, ExportWritePolicy
13
13
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
14
+ from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
14
15
  from mage_ai.shared.hash import merge_dict
15
16
 
16
17
  DEFAULT_LOGIN_TIMEOUT = 20
@@ -201,6 +202,8 @@ class Snowflake(BaseSQLConnection):
201
202
  schema: str = None,
202
203
  if_exists: str = 'append',
203
204
  query_string: Union[str, None] = None,
205
+ unique_conflict_method: str = None,
206
+ unique_constraints: List[str] = None,
204
207
  verbose: bool = True,
205
208
  **kwargs,
206
209
  ) -> None:
@@ -265,37 +268,28 @@ class Snowflake(BaseSQLConnection):
265
268
  cur.execute(f'DROP TABLE "{schema}"."{table_name}"', timeout=self.timeout)
266
269
  should_create_table = True
267
270
 
268
- if query_string:
269
- cur.execute(f'USE DATABASE {database}', timeout=self.timeout)
270
- cur.execute(f'USE SCHEMA {schema}', timeout=self.timeout)
271
-
272
- if should_create_table:
273
- cur.execute(f"""
274
- CREATE TABLE IF NOT EXISTS "{database}"."{schema}"."{table_name}" AS
275
- {query_string}
276
- """, timeout=self.timeout)
277
- else:
278
- cur.execute(f"""
279
- INSERT INTO "{database}"."{schema}"."{table_name}"
280
- {query_string}
281
- """, timeout=self.timeout)
282
-
283
- else:
284
- write_kwargs = merge_dict(
285
- dict(
286
- auto_create_table=should_create_table,
287
- database=database,
288
- schema=schema,
289
- # This param makes sure datetime column is written correctly
290
- use_logical_type=True,
291
- ),
292
- kwargs or dict(),
293
- )
294
- write_pandas(
295
- self.conn,
271
+ if unique_constraints and unique_conflict_method and df is not None:
272
+ self.__upsert_df_into_table(
273
+ table_name,
296
274
  df,
275
+ cur,
276
+ database,
277
+ schema,
278
+ should_create_table=should_create_table,
279
+ unique_conflict_method=unique_conflict_method,
280
+ unique_constraints=unique_constraints,
281
+ **kwargs,
282
+ )
283
+ else:
284
+ self.__write_table(
297
285
  table_name,
298
- **write_kwargs,
286
+ df,
287
+ cur,
288
+ database,
289
+ schema,
290
+ should_create_table=should_create_table,
291
+ query_string=query_string,
292
+ **kwargs,
299
293
  )
300
294
 
301
295
  if verbose:
@@ -306,6 +300,135 @@ INSERT INTO "{database}"."{schema}"."{table_name}"
306
300
  else:
307
301
  __process()
308
302
 
303
+ def __upsert_df_into_table(
304
+ self,
305
+ table_name: str,
306
+ df: DataFrame,
307
+ cursor,
308
+ database: str,
309
+ schema: str,
310
+ should_create_table: bool = False,
311
+ unique_conflict_method: str = None,
312
+ unique_constraints: List[str] = None,
313
+ allow_reserved_words: bool = True,
314
+ auto_clean_name: bool = True,
315
+ case_sensitive: bool = True,
316
+ **kwargs
317
+ ):
318
+ write_kwargs = merge_dict(
319
+ dict(
320
+ auto_create_table=True,
321
+ database=database,
322
+ schema=schema,
323
+ # This param makes sure datetime column is written correctly
324
+ use_logical_type=True,
325
+ ),
326
+ kwargs or dict(),
327
+ )
328
+ # should_create_table is True when the table does not exist, so just create the
329
+ # table as normal.
330
+ if should_create_table:
331
+ write_pandas(
332
+ self.conn,
333
+ df,
334
+ table_name,
335
+ **write_kwargs,
336
+ )
337
+ else:
338
+ temp_table_name = f'temp_{table_name}'
339
+ write_pandas(
340
+ self.conn,
341
+ df,
342
+ temp_table_name,
343
+ table_type='temp',
344
+ **write_kwargs,
345
+ )
346
+
347
+ cleaned_unique_constraints = []
348
+ for col in unique_constraints:
349
+ cleaned_col = self._clean_column_name(
350
+ col,
351
+ allow_reserved_words=allow_reserved_words,
352
+ auto_clean_name=auto_clean_name,
353
+ case_sensitive=case_sensitive,
354
+ )
355
+ cleaned_unique_constraints.append(f'"{cleaned_col}"')
356
+
357
+ cleaned_columns = []
358
+ for col in df.columns:
359
+ cleaned_col = self._clean_column_name(
360
+ col,
361
+ allow_reserved_words=allow_reserved_words,
362
+ auto_clean_name=auto_clean_name,
363
+ case_sensitive=case_sensitive,
364
+ )
365
+ cleaned_columns.append(f'"{cleaned_col}"')
366
+
367
+ merge_commands = [
368
+ f'MERGE INTO "{database}"."{schema}"."{table_name}" AS a',
369
+ f'USING (SELECT * FROM "{database}"."{schema}"."{temp_table_name}") AS b',
370
+ f"ON {' AND '.join([f'a.{col} = b.{col}' for col in cleaned_unique_constraints])}",
371
+ ]
372
+
373
+ if unique_conflict_method == UNIQUE_CONFLICT_METHOD_UPDATE:
374
+ set_command = ', '.join(
375
+ [f'a.{col} = b.{col}' for col in cleaned_columns],
376
+ )
377
+ merge_commands.append(f'WHEN MATCHED THEN UPDATE SET {set_command}')
378
+
379
+ insert_columns = ', '.join(cleaned_columns)
380
+ merge_values = f"({', '.join([f'b.{col}' for col in cleaned_columns])})"
381
+ merge_commands.append(
382
+ f"WHEN NOT MATCHED THEN INSERT ({insert_columns}) VALUES {merge_values}",
383
+ )
384
+ merge_command = '\n'.join(merge_commands)
385
+
386
+ cursor.execute(merge_command, timeout=self.timeout)
387
+
388
+ def __write_table(
389
+ self,
390
+ table_name: str,
391
+ df: DataFrame,
392
+ cursor,
393
+ database: str,
394
+ schema: str,
395
+ should_create_table: bool = False,
396
+ query_string: str = None,
397
+ **kwargs
398
+ ):
399
+ if query_string:
400
+ cursor.execute(f'USE DATABASE {database}', timeout=self.timeout)
401
+ cursor.execute(f'USE SCHEMA {schema}', timeout=self.timeout)
402
+
403
+ if should_create_table:
404
+ cursor.execute(f"""
405
+ CREATE TABLE IF NOT EXISTS "{database}"."{schema}"."{table_name}" AS
406
+ {query_string}
407
+ """, timeout=self.timeout)
408
+ else:
409
+ cursor.execute(f"""
410
+ INSERT INTO "{database}"."{schema}"."{table_name}"
411
+ {query_string}
412
+ """, timeout=self.timeout)
413
+
414
+ else:
415
+ write_kwargs = merge_dict(
416
+ dict(
417
+ auto_create_table=should_create_table,
418
+ database=database,
419
+ schema=schema,
420
+ # This param makes sure datetime column is written correctly
421
+ use_logical_type=True,
422
+ ),
423
+ kwargs or dict(),
424
+ )
425
+ write_pandas(
426
+ self.conn,
427
+ df,
428
+ table_name,
429
+ **write_kwargs,
430
+ )
431
+
309
432
  @classmethod
310
433
  def with_config(
311
434
  cls,
mage_ai/io/sql.py CHANGED
@@ -56,6 +56,7 @@ class BaseSQL(BaseSQLConnection):
56
56
  case_sensitive: bool = False,
57
57
  unique_constraints: List[str] = None,
58
58
  overwrite_types: Dict = None,
59
+ skip_semicolon_at_end: bool = False,
59
60
  **kwargs,
60
61
  ) -> str:
61
62
  if unique_constraints is None:
@@ -68,6 +69,7 @@ class BaseSQL(BaseSQLConnection):
68
69
  case_sensitive=case_sensitive,
69
70
  unique_constraints=unique_constraints,
70
71
  overwrite_types=overwrite_types,
72
+ skip_semicolon_at_end=skip_semicolon_at_end,
71
73
  )
72
74
 
73
75
  def build_create_table_as_command(
@@ -234,6 +236,7 @@ class BaseSQL(BaseSQLConnection):
234
236
  query_string: Union[str, None] = None,
235
237
  unique_conflict_method: str = None,
236
238
  unique_constraints: List[str] = None,
239
+ skip_semicolon_at_end: bool = False,
237
240
  **kwargs,
238
241
  ) -> None:
239
242
  """
@@ -350,6 +353,7 @@ class BaseSQL(BaseSQLConnection):
350
353
  case_sensitive=case_sensitive,
351
354
  unique_constraints=unique_constraints,
352
355
  overwrite_types=overwrite_types,
356
+ skip_semicolon_at_end=skip_semicolon_at_end,
353
357
  )
354
358
  cur.execute(query)
355
359
  self.upload_dataframe(
@@ -13,7 +13,7 @@ from mage_ai.orchestration.db.setup import get_postgres_connection_url
13
13
  from mage_ai.orchestration.db.utils import get_user_info_from_db_connection_url
14
14
  from mage_ai.settings import OTEL_EXPORTER_OTLP_ENDPOINT
15
15
  from mage_ai.settings.repo import get_variables_dir
16
- from mage_ai.shared.environments import is_dev, is_test
16
+ from mage_ai.shared.environments import is_debug, is_test
17
17
 
18
18
  DB_RETRY_COUNT = 2
19
19
  TEST_DB = 'test.db'
@@ -170,5 +170,5 @@ def safe_db_query(func):
170
170
 
171
171
  logging.basicConfig()
172
172
 
173
- if is_dev() and not os.getenv('DISABLE_DATABASE_TERMINAL_OUTPUT'):
173
+ if is_debug() and not os.getenv('DISABLE_DATABASE_TERMINAL_OUTPUT'):
174
174
  logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
@@ -1,7 +1,7 @@
1
1
  import enum
2
2
  import re
3
3
  from datetime import datetime
4
- from typing import Dict, List, Union
4
+ from typing import Callable, Dict, List, Union
5
5
 
6
6
  from sqlalchemy import (
7
7
  JSON,
@@ -327,7 +327,7 @@ class Role(BaseModel):
327
327
  self,
328
328
  entity: Entity = None,
329
329
  entity_id: str = None,
330
- prefix: str = None,
330
+ name_func: Callable[[str], str] = None,
331
331
  ) -> None:
332
332
  """
333
333
  Create default roles with associated permissions for a given entity and entity_id.
@@ -349,8 +349,8 @@ class Role(BaseModel):
349
349
  }
350
350
  for name, access in mapping.items():
351
351
  role_name = name
352
- if prefix is not None:
353
- role_name = f'{prefix}_{name}'
352
+ if name_func is not None:
353
+ role_name = name_func(name)
354
354
  role = self.query.filter(self.name == role_name).first()
355
355
  if not role:
356
356
  self.create(
@@ -48,6 +48,7 @@ from mage_ai.data_preparation.models.constants import (
48
48
  PipelineType,
49
49
  )
50
50
  from mage_ai.data_preparation.models.pipeline import Pipeline
51
+ from mage_ai.data_preparation.models.project import Project
51
52
  from mage_ai.data_preparation.models.triggers import (
52
53
  ScheduleInterval,
53
54
  ScheduleStatus,
@@ -934,9 +935,12 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
934
935
 
935
936
  @property
936
937
  def pipeline_tags(self):
937
- pipeline = Pipeline.get(self.pipeline_uuid, check_if_exists=True)
938
+ try:
939
+ pipeline_config = Pipeline.get_config(self.pipeline_uuid)
940
+ except Exception:
941
+ pipeline_config = dict()
938
942
 
939
- return pipeline.tags if pipeline is not None else []
943
+ return pipeline_config.get('tags') if pipeline_config is not None else []
940
944
 
941
945
  def executable_block_runs(
942
946
  self,
@@ -989,6 +993,9 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
989
993
  block_runs_all = []
990
994
 
991
995
  data_integration_block_uuids_mapping = {}
996
+
997
+ pipeline_project = Project(pipeline.repo_config)
998
+
992
999
  for block_run in self.block_runs:
993
1000
  block_runs_all.append(block_run)
994
1001
 
@@ -1021,7 +1028,7 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
1021
1028
  "original": 1,
1022
1029
  }
1023
1030
  """
1024
- if metrics and block and block.is_data_integration():
1031
+ if metrics and block and block.is_data_integration(pipeline_project=pipeline_project):
1025
1032
  original_block_uuid = metrics.get('original_block_uuid')
1026
1033
 
1027
1034
  if original_block_uuid and metrics.get('child'):
@@ -86,6 +86,12 @@ class JobManager:
86
86
  job_id = self.__job_id(JobType.INTEGRATION_STREAM, id)
87
87
  return self.queue.kill_job(job_id)
88
88
 
89
+ def start(self):
90
+ self.queue.start()
91
+
92
+ def stop(self):
93
+ self.queue.stop()
94
+
89
95
  def __job_id(self, job_type: JobType, uid: Union[str, int]):
90
96
  return f'{job_type}_{uid}'
91
97
 
@@ -108,6 +108,7 @@ class NotificationSender:
108
108
  pipeline,
109
109
  pipeline_run,
110
110
  error: str = None,
111
+ stacktrace: str = None,
111
112
  summary: str = None,
112
113
  ) -> None:
113
114
  if AlertOn.PIPELINE_RUN_FAILURE in self.config.alert_on:
@@ -122,6 +123,7 @@ class NotificationSender:
122
123
  pipeline_run,
123
124
  error=error,
124
125
  message_template=message_template,
126
+ stacktrace=stacktrace,
125
127
  summary=summary,
126
128
  )
127
129
 
@@ -145,6 +147,7 @@ class NotificationSender:
145
147
  pipeline,
146
148
  pipeline_run,
147
149
  error: str = None,
150
+ stacktrace: str = None,
148
151
  ):
149
152
  if text is None or pipeline is None or pipeline_run is None:
150
153
  return text
@@ -155,6 +158,7 @@ class NotificationSender:
155
158
  pipeline_schedule_id=pipeline_run.pipeline_schedule.id,
156
159
  pipeline_schedule_name=pipeline_run.pipeline_schedule.name,
157
160
  pipeline_uuid=pipeline.uuid,
161
+ stacktrace=stacktrace,
158
162
  )
159
163
 
160
164
  def __send_pipeline_run_message(
@@ -164,6 +168,7 @@ class NotificationSender:
164
168
  pipeline_run,
165
169
  error: str = None,
166
170
  message_template: MessageTemplate = None,
171
+ stacktrace: str = None,
167
172
  summary: str = None,
168
173
  ):
169
174
  """Shared method to send pipeline run message of multiple types (success, failure, etc.).
@@ -207,18 +212,21 @@ class NotificationSender:
207
212
  pipeline,
208
213
  pipeline_run,
209
214
  error=error,
215
+ stacktrace=stacktrace,
210
216
  ),
211
217
  summary=self.__interpolate_vars(
212
218
  summary or default_summary,
213
219
  pipeline,
214
220
  pipeline_run,
215
221
  error=error,
222
+ stacktrace=stacktrace,
216
223
  ),
217
224
  details=self.__interpolate_vars(
218
225
  details or default_details,
219
226
  pipeline,
220
227
  pipeline_run,
221
228
  error=error,
229
+ stacktrace=stacktrace,
222
230
  ),
223
231
  )
224
232
 
@@ -320,11 +320,19 @@ class PipelineScheduler:
320
320
  @safe_db_query
321
321
  def on_pipeline_run_failure(self, error_msg: str) -> None:
322
322
  failed_block_runs = self.pipeline_run.failed_block_runs
323
+ stacktrace = None
323
324
  for br in failed_block_runs:
324
325
  if br.metrics:
325
326
  message = br.metrics.get('error', {}).get('message')
326
327
  if message:
327
- error_msg += f'\nError for block {br.block_uuid}:\n{message}'
328
+ message_split = message.split('\n')
329
+ # Truncate the error message if it has too many lines, set max
330
+ # lines at 50
331
+ if len(message_split) > 50:
332
+ message_split = message_split[-50:]
333
+ message_split.insert(0, '... (error truncated)')
334
+ message = '\n'.join(message_split)
335
+ stacktrace = f'Error for block {br.block_uuid}:\n{message}'
328
336
  break
329
337
 
330
338
  asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
@@ -332,6 +340,7 @@ class PipelineScheduler:
332
340
  pipeline=self.pipeline,
333
341
  pipeline_run=self.pipeline_run,
334
342
  error=error_msg,
343
+ stacktrace=stacktrace,
335
344
  )
336
345
  # Cancel block runs that are still in progress for the pipeline run.
337
346
  cancel_block_runs_and_jobs(self.pipeline_run, self.pipeline)
@@ -821,6 +830,8 @@ class PipelineScheduler:
821
830
  Returns:
822
831
  List[BlockRun]: A list of crashed block runs.
823
832
  """
833
+ for b in self.pipeline_run.block_runs:
834
+ b.refresh()
824
835
  running_or_queued_block_runs = [b for b in self.pipeline_run.block_runs if b.status in [
825
836
  BlockRun.BlockRunStatus.RUNNING,
826
837
  BlockRun.BlockRunStatus.QUEUED,
@@ -1670,7 +1681,11 @@ def gen_pipeline_with_schedules_single_project(
1670
1681
  # Iterate through pipeline schedules by pipeline to handle pipeline run limits for
1671
1682
  # each pipeline.
1672
1683
  for pipeline_uuid, active_schedules in pipeline_schedules_by_pipeline.items():
1673
- pipeline = Pipeline.get(pipeline_uuid)
1684
+ try:
1685
+ pipeline = Pipeline.get(pipeline_uuid)
1686
+ except Exception as e:
1687
+ print(f'Error fetching pipeline {pipeline_uuid}: {e}')
1688
+ continue
1674
1689
  yield pipeline_uuid, pipeline, active_schedules
1675
1690
 
1676
1691
 
@@ -1731,11 +1746,15 @@ def gen_pipeline_with_schedules_project_platform(
1731
1746
  for pair in pipeline_schedules_by_pipeline_by_repo_path.items():
1732
1747
  repo_path, pipeline_schedules_by_pipeline = pair
1733
1748
  for pipeline_uuid, active_schedules in pipeline_schedules_by_pipeline.items():
1734
- pipeline = get_pipeline_from_platform(
1735
- pipeline_uuid,
1736
- repo_path=repo_path,
1737
- mapping=pipeline_schedule_repo_paths_to_repo_path_mapping,
1738
- )
1749
+ try:
1750
+ pipeline = get_pipeline_from_platform(
1751
+ pipeline_uuid,
1752
+ repo_path=repo_path,
1753
+ mapping=pipeline_schedule_repo_paths_to_repo_path_mapping,
1754
+ )
1755
+ except Exception as e:
1756
+ print(f'Error fetching pipeline {pipeline_uuid}: {e}')
1757
+ continue
1739
1758
  yield pipeline_uuid, pipeline, active_schedules
1740
1759
 
1741
1760
 
@@ -1,6 +1,7 @@
1
+ from typing import Callable
2
+
1
3
  from mage_ai.orchestration.queue.config import QueueConfig
2
4
  from mage_ai.orchestration.queue.queue import Queue
3
- from typing import Callable
4
5
 
5
6
 
6
7
  class CeleryQueue(Queue):
@@ -27,3 +28,9 @@ class CeleryQueue(Queue):
27
28
 
28
29
  def kill_job(self, job_id: str):
29
30
  pass
31
+
32
+ def start(self):
33
+ pass
34
+
35
+ def stop(self):
36
+ pass
@@ -36,6 +36,11 @@ class JobStatus(str, Enum):
36
36
  CANCELLED = 'cancelled'
37
37
 
38
38
 
39
+ class QueueStatus(str, Enum):
40
+ ACTIVE = 'active'
41
+ INACTIVE = 'inactive'
42
+
43
+
39
44
  class ProcessQueue(Queue):
40
45
  def __init__(self, queue_config: QueueConfig):
41
46
  """
@@ -53,6 +58,7 @@ class ProcessQueue(Queue):
53
58
  jobs.
54
59
 
55
60
  """
61
+ self.status = QueueStatus.INACTIVE
56
62
  self.queue_config = queue_config
57
63
  self.process_queue_config = self.queue_config.process_queue_config
58
64
  self.queue = mp.Queue()
@@ -76,12 +82,16 @@ class ProcessQueue(Queue):
76
82
 
77
83
  def clean_up_jobs(self):
78
84
  """
79
- Cleans up completed jobs from the job dictionary.
85
+ 1. Cleans up completed jobs from the job dictionary.
86
+ 2. Check whether there're jobs need to be killed.
80
87
  """
81
88
  job_ids = self.job_dict.keys()
82
89
  for job_id in job_ids:
83
- if job_id in self.job_dict and not self.has_job(job_id):
84
- del self.job_dict[job_id]
90
+ if job_id in self.job_dict:
91
+ if not self.has_job(job_id):
92
+ del self.job_dict[job_id]
93
+ elif self.__should_kill_job(job_id):
94
+ self.kill_job(job_id)
85
95
 
86
96
  def enqueue(self, job_id: str, target: Callable, *args, **kwargs):
87
97
  """
@@ -94,6 +104,9 @@ class ProcessQueue(Queue):
94
104
  **kwargs: Keyword arguments for the target function.
95
105
 
96
106
  """
107
+ if self.status != QueueStatus.ACTIVE:
108
+ self._print('Cannot enqueue a job to an inactive queue.')
109
+ return
97
110
  if self.has_job(job_id):
98
111
  self._print(f'Job {job_id} exists. Skip enqueue.')
99
112
  return
@@ -155,6 +168,7 @@ class ProcessQueue(Queue):
155
168
  print(f'Kill job {job_id}, job_dict {self.job_dict}')
156
169
  job = self.job_dict.get(job_id)
157
170
  if not job:
171
+ self.__set_kill_job(job_id)
158
172
  return
159
173
  if isinstance(job, int):
160
174
  if job == os.getpid():
@@ -165,6 +179,7 @@ class ProcessQueue(Queue):
165
179
  except Exception as err:
166
180
  print(err)
167
181
  self.job_dict[job_id] = JobStatus.CANCELLED
182
+ self.__unset_kill_job(job_id)
168
183
 
169
184
  def start_worker_pool(self):
170
185
  """
@@ -182,6 +197,28 @@ class ProcessQueue(Queue):
182
197
  )
183
198
  self.worker_pool_proc.start()
184
199
 
200
+ def start(self):
201
+ self.status = QueueStatus.ACTIVE
202
+
203
+ def stop(self):
204
+ """
205
+ 1. Stop enqueueing new jobs
206
+ 2. Clear the queue
207
+ 3. Kill all the running jobs
208
+ """
209
+ self.status = QueueStatus.INACTIVE
210
+ while not self.queue.empty():
211
+ try:
212
+ self.queue.get_nowait()
213
+ except self.queue.Empty:
214
+ break
215
+ job_ids = self.job_dict.keys()
216
+ for job_id in job_ids:
217
+ if job_id in self.job_dict:
218
+ if isinstance(self.job_dict.get(job_id), int):
219
+ self.kill_job(job_id)
220
+ del self.job_dict[job_id]
221
+
185
222
  def is_worker_pool_alive(self) -> bool:
186
223
  """
187
224
  Checks if the worker pool process is alive.
@@ -197,6 +234,31 @@ class ProcessQueue(Queue):
197
234
  def __is_process_alive(self, pid: int) -> bool:
198
235
  return psutil.pid_exists(pid)
199
236
 
237
+ def __redis_key_kill_job(self, job_id):
238
+ return f'kill_job_{job_id}'
239
+
240
+ def __set_kill_job(self, job_id):
241
+ if not self.redis_client:
242
+ return
243
+ return self.redis_client.set(
244
+ self.__redis_key_kill_job(job_id),
245
+ '1',
246
+ ex=LIVENESS_TIMEOUT_SECONDS,
247
+ )
248
+
249
+ def __unset_kill_job(self, job_id):
250
+ if not self.redis_client:
251
+ return
252
+ key = self.__redis_key_kill_job(job_id)
253
+ if self.redis_client.get(key):
254
+ self.redis_client.delete(key)
255
+
256
+ def __should_kill_job(self, job_id):
257
+ if not self.redis_client:
258
+ return False
259
+ value = self.redis_client.get(self.__redis_key_kill_job(job_id))
260
+ return value is not None
261
+
200
262
 
201
263
  class Worker(mp.Process):
202
264
  def __init__(
@@ -275,10 +337,11 @@ def poll_job_and_execute(
275
337
  job_dict: The shared job dictionary.
276
338
 
277
339
  """
340
+ pid = os.getpid()
278
341
  workers = []
279
342
  while True:
280
343
  workers = [w for w in workers if w.is_alive()]
281
- print(f'Worker pool size: {len(workers)}')
344
+ print(f'[Process {pid}] Worker pool size: {len(workers)}')
282
345
  if not workers and queue.empty():
283
346
  break
284
347
  while not queue.empty():
@@ -19,5 +19,13 @@ class Queue(ABC):
19
19
  def kill_job(self, job_id: str):
20
20
  pass
21
21
 
22
+ @abstractmethod
23
+ def start(self):
24
+ pass
25
+
26
+ @abstractmethod
27
+ def stop(self):
28
+ pass
29
+
22
30
  def _print(self, msg):
23
31
  print(f'[{self.__class__.__name__}] {msg}')
@@ -12,4 +12,4 @@ DATAFRAME_OUTPUT_SAMPLE_COUNT = 10
12
12
  # Dockerfile depends on it because it runs ./scripts/install_mage.sh and uses
13
13
  # the last line to determine the version to install.
14
14
  VERSION = \
15
- '0.9.67'
15
+ '0.9.69'