mage-ai 0.9.65__py3-none-any.whl → 0.9.67__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 (242) hide show
  1. mage_ai/api/monitors/BaseMonitor.py +38 -16
  2. mage_ai/api/policies/BackfillPolicy.py +1 -0
  3. mage_ai/api/resources/OauthResource.py +13 -5
  4. mage_ai/api/resources/SessionResource.py +6 -4
  5. mage_ai/authentication/ldap.py +19 -9
  6. mage_ai/authentication/oauth/constants.py +8 -14
  7. mage_ai/authentication/oauth/utils.py +18 -6
  8. mage_ai/authentication/providers/active_directory.py +21 -16
  9. mage_ai/authentication/providers/azure_devops.py +18 -0
  10. mage_ai/authentication/providers/bitbucket.py +10 -9
  11. mage_ai/authentication/providers/constants.py +2 -0
  12. mage_ai/authentication/providers/ghe.py +5 -9
  13. mage_ai/authentication/providers/gitlab.py +6 -9
  14. mage_ai/authentication/providers/google.py +9 -6
  15. mage_ai/authentication/providers/oidc.py +6 -4
  16. mage_ai/authentication/providers/okta.py +9 -6
  17. mage_ai/cluster_manager/kubernetes/workload_manager.py +10 -0
  18. mage_ai/cluster_manager/workspace/base.py +6 -1
  19. mage_ai/cluster_manager/workspace/kubernetes.py +3 -0
  20. mage_ai/data_preparation/decorators.py +15 -0
  21. mage_ai/data_preparation/executors/streaming_pipeline_executor.py +22 -12
  22. mage_ai/data_preparation/git/__init__.py +10 -1
  23. mage_ai/data_preparation/git/api.py +3 -0
  24. mage_ai/data_preparation/git/clients/azure_devops.py +106 -0
  25. mage_ai/data_preparation/git/clients/base.py +6 -0
  26. mage_ai/data_preparation/git/clients/gitlab.py +3 -2
  27. mage_ai/data_preparation/git/utils.py +31 -29
  28. mage_ai/data_preparation/models/block/__init__.py +27 -18
  29. mage_ai/data_preparation/models/block/dbt/block_sql.py +164 -0
  30. mage_ai/data_preparation/models/block/dynamic/variables.py +1 -2
  31. mage_ai/data_preparation/models/pipeline.py +3 -3
  32. mage_ai/data_preparation/models/triggers/__init__.py +6 -1
  33. mage_ai/data_preparation/preferences.py +42 -37
  34. mage_ai/data_preparation/repo_manager.py +21 -0
  35. mage_ai/data_preparation/storage/gcs_storage.py +27 -2
  36. mage_ai/data_preparation/storage/local_storage.py +18 -3
  37. mage_ai/data_preparation/storage/s3_storage.py +7 -2
  38. mage_ai/data_preparation/templates/data_loaders/streaming/generic_python.py +23 -0
  39. mage_ai/data_preparation/templates/main/metadata.yaml +6 -0
  40. mage_ai/data_preparation/templates/template.py +6 -2
  41. mage_ai/data_preparation/variable_manager.py +2 -1
  42. mage_ai/io/base.py +3 -0
  43. mage_ai/io/bigquery.py +2 -0
  44. mage_ai/io/export_utils.py +14 -9
  45. mage_ai/io/mssql.py +104 -25
  46. mage_ai/io/mysql.py +10 -9
  47. mage_ai/io/oracledb.py +14 -2
  48. mage_ai/io/postgres.py +3 -0
  49. mage_ai/io/sql.py +14 -6
  50. mage_ai/io/trino.py +10 -8
  51. mage_ai/orchestration/db/migrations/versions/90d978a8aef8_update_unique_constraint_for_secret.py +11 -5
  52. mage_ai/orchestration/db/models/schedules.py +25 -1
  53. mage_ai/orchestration/db/models/schedules_project_platform.py +24 -1
  54. mage_ai/orchestration/job_manager.py +6 -1
  55. mage_ai/orchestration/pipeline_scheduler_original.py +16 -10
  56. mage_ai/server/constants.py +1 -1
  57. mage_ai/server/file_observer.py +10 -0
  58. mage_ai/server/frontend_dist/404.html +2 -2
  59. mage_ai/server/frontend_dist/_next/static/chunks/{1557-a754b04510d50b80.js → 1557-01f0843dc6ac4971.js} +1 -1
  60. mage_ai/server/frontend_dist/_next/static/chunks/9440-4069842b90d4b801.js +1 -0
  61. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-1c1ffd928f5a00f7.js +1 -0
  62. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-b7b8695a7f9efde2.js +1 -0
  63. mage_ai/server/frontend_dist/_next/static/chunks/pages/oauth-30e34ee15d410331.js +1 -0
  64. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-ff4bd7a8ec3bab40.js +1 -0
  65. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-aaf393c86fc1bda3.js +1 -0
  66. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{edit-8d32ac7e8f023779.js → edit-36377e679da2cd91.js} +1 -1
  67. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-3f5c14076ddde20e.js +1 -0
  68. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-fe08120d9a6fb1b0.js → triggers-f508c2f261297724.js} +1 -1
  69. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-bcdb4ad41dd4c7d5.js → frontend_dist/_next/static/chunks/pages/pipelines-f99e99aa8f45529c.js} +1 -1
  70. mage_ai/server/frontend_dist/_next/static/chunks/pages/{sign-in-19b36600d908b711.js → sign-in-7d38b2f7c3e918a1.js} +1 -1
  71. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-5753fac7c1bfdc88.js +1 -0
  72. mage_ai/server/frontend_dist/_next/static/chunks/{webpack-ac7fdc472bedf682.js → webpack-d079359c241db804.js} +1 -1
  73. mage_ai/server/frontend_dist/_next/static/{ZMrJfDouIX5AMb_RteRbL → vPsMu6Fi2zrHaf2fRXKRO}/_buildManifest.js +1 -1
  74. mage_ai/server/frontend_dist/block-layout.html +2 -2
  75. mage_ai/server/frontend_dist/compute.html +2 -2
  76. mage_ai/server/frontend_dist/files.html +2 -2
  77. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  78. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  79. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  80. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  81. mage_ai/server/frontend_dist/index.html +2 -2
  82. mage_ai/server/frontend_dist/manage/files.html +2 -2
  83. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  84. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  85. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  86. mage_ai/server/frontend_dist/manage/users.html +2 -2
  87. mage_ai/server/frontend_dist/manage.html +2 -2
  88. mage_ai/server/frontend_dist/oauth.html +2 -2
  89. mage_ai/server/frontend_dist/overview.html +2 -2
  90. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  91. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  92. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  93. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  94. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  95. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  96. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  97. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  98. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  99. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  100. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  101. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  102. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  103. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  104. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  105. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  106. mage_ai/server/frontend_dist/pipelines.html +2 -2
  107. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
  108. mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
  109. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  110. mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
  111. mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
  112. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  113. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  114. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  115. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  116. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  117. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  118. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  119. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  120. mage_ai/server/frontend_dist/settings.html +2 -2
  121. mage_ai/server/frontend_dist/sign-in.html +2 -2
  122. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  123. mage_ai/server/frontend_dist/templates.html +2 -2
  124. mage_ai/server/frontend_dist/terminal.html +2 -2
  125. mage_ai/server/frontend_dist/test.html +2 -2
  126. mage_ai/server/frontend_dist/triggers.html +2 -2
  127. mage_ai/server/frontend_dist/version-control.html +2 -2
  128. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  129. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{1557-a754b04510d50b80.js → 1557-01f0843dc6ac4971.js} +1 -1
  130. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9440-4069842b90d4b801.js +1 -0
  131. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-1c1ffd928f5a00f7.js +1 -0
  132. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-b7b8695a7f9efde2.js +1 -0
  133. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/oauth-30e34ee15d410331.js +1 -0
  134. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-ff4bd7a8ec3bab40.js +1 -0
  135. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-aaf393c86fc1bda3.js +1 -0
  136. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{edit-8d32ac7e8f023779.js → edit-36377e679da2cd91.js} +1 -1
  137. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-3f5c14076ddde20e.js +1 -0
  138. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-fe08120d9a6fb1b0.js → triggers-f508c2f261297724.js} +1 -1
  139. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines-bcdb4ad41dd4c7d5.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-f99e99aa8f45529c.js} +1 -1
  140. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{sign-in-19b36600d908b711.js → sign-in-7d38b2f7c3e918a1.js} +1 -1
  141. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-5753fac7c1bfdc88.js +1 -0
  142. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{webpack-481689d9989710cd.js → webpack-68c003fb6a175cd7.js} +1 -1
  143. mage_ai/server/frontend_dist_base_path_template/_next/static/{QYwFH4sievaq5XyUjRriy → khKiaJtwrslgMmp4YSa1f}/_buildManifest.js +1 -1
  144. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  145. mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
  146. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  147. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  148. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  149. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
  150. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
  151. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  152. mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
  153. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  154. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  155. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  156. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  157. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  158. mage_ai/server/frontend_dist_base_path_template/oauth.html +2 -2
  159. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  160. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  161. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  162. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  163. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
  164. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  165. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  166. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  167. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  168. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  169. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  170. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  171. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  172. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  173. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  174. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  175. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  176. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  177. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
  178. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
  179. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  180. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
  181. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
  182. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
  183. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
  184. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  185. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
  186. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
  187. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  188. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
  189. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  190. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  191. mage_ai/server/frontend_dist_base_path_template/sign-in.html +2 -2
  192. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  193. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  194. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  195. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  196. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  197. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  198. mage_ai/server/server.py +117 -87
  199. mage_ai/server/utils/output_display.py +6 -1
  200. mage_ai/services/aws/s3/s3.py +8 -2
  201. mage_ai/services/slack/slack.py +8 -8
  202. mage_ai/settings/__init__.py +36 -186
  203. mage_ai/settings/backends.py +95 -0
  204. mage_ai/settings/keys/__init__.py +1 -0
  205. mage_ai/settings/keys/auth.py +76 -0
  206. mage_ai/settings/server.py +187 -0
  207. mage_ai/shared/io.py +2 -2
  208. mage_ai/shared/logger.py +12 -6
  209. mage_ai/streaming/sources/base_python.py +30 -0
  210. mage_ai/streaming/sources/source_factory.py +25 -0
  211. mage_ai/tests/api/endpoints/test_oauths.py +13 -5
  212. mage_ai/tests/api/operations/test_operations.py +2 -2
  213. mage_ai/tests/api/operations/test_sessions.py +83 -48
  214. mage_ai/tests/authentication/oauth/test_utils.py +56 -6
  215. mage_ai/tests/authentication/providers/test_active_directory.py +9 -15
  216. mage_ai/tests/data_preparation/models/test_block.py +39 -2
  217. mage_ai/tests/orchestration/db/models/test_schedules.py +33 -1
  218. {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/METADATA +2 -1
  219. {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/RECORD +225 -217
  220. {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/WHEEL +1 -1
  221. mage_ai/server/frontend_dist/_next/static/chunks/9440-2bcbdc765ed82062.js +0 -1
  222. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-2ae1d919333f01fe.js +0 -1
  223. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-64851458dde54ad9.js +0 -1
  224. mage_ai/server/frontend_dist/_next/static/chunks/pages/oauth-abe5ba687cb93509.js +0 -1
  225. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-22e49726eeed16ae.js +0 -1
  226. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-c74507dce89b41a2.js +0 -1
  227. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-cf656cbe37ecaacc.js +0 -1
  228. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-690206d30d8b412b.js +0 -1
  229. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9440-2bcbdc765ed82062.js +0 -1
  230. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-2ae1d919333f01fe.js +0 -1
  231. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-64851458dde54ad9.js +0 -1
  232. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/oauth-abe5ba687cb93509.js +0 -1
  233. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-22e49726eeed16ae.js +0 -1
  234. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-c74507dce89b41a2.js +0 -1
  235. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-cf656cbe37ecaacc.js +0 -1
  236. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-690206d30d8b412b.js +0 -1
  237. mage_ai/settings/sso.py +0 -27
  238. /mage_ai/server/frontend_dist/_next/static/{ZMrJfDouIX5AMb_RteRbL → vPsMu6Fi2zrHaf2fRXKRO}/_ssgManifest.js +0 -0
  239. /mage_ai/server/frontend_dist_base_path_template/_next/static/{QYwFH4sievaq5XyUjRriy → khKiaJtwrslgMmp4YSa1f}/_ssgManifest.js +0 -0
  240. {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/LICENSE +0 -0
  241. {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/entry_points.txt +0 -0
  242. {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/top_level.txt +0 -0
@@ -104,6 +104,7 @@ def gen_table_creation_query(
104
104
  dtypes: Mapping[str, str],
105
105
  schema_name: str,
106
106
  table_name: str,
107
+ auto_clean_name: bool = True,
107
108
  case_sensitive: bool = False,
108
109
  unique_constraints: List[str] = None,
109
110
  overwrite_types: Dict = None,
@@ -123,16 +124,16 @@ def gen_table_creation_query(
123
124
  if unique_constraints is None:
124
125
  unique_constraints = []
125
126
  query = []
126
- if overwrite_types is not None:
127
+ for cname in dtypes:
128
+ if overwrite_types is not None and cname in overwrite_types.keys():
129
+ dtypes[cname] = overwrite_types[cname]
127
130
 
128
- for cname in dtypes:
129
- if cname in overwrite_types.keys():
130
- dtypes[cname] = overwrite_types[cname]
131
+ if auto_clean_name:
132
+ cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
133
+ else:
134
+ cleaned_col_name = cname
131
135
 
132
- query.append(f'"{clean_name(cname, case_sensitive=case_sensitive)}" {dtypes[cname]}')
133
- else:
134
- for cname in dtypes:
135
- query.append(f'"{clean_name(cname, case_sensitive=case_sensitive)}" {dtypes[cname]}')
136
+ query.append(f'"{cleaned_col_name}" {dtypes[cname]}')
136
137
 
137
138
  if schema_name:
138
139
  full_table_name = f'{schema_name}.{table_name}'
@@ -142,7 +143,11 @@ def gen_table_creation_query(
142
143
  if unique_constraints:
143
144
  unique_constraints_clean = []
144
145
  for col in unique_constraints:
145
- unique_constraints_clean.append(clean_name(col, case_sensitive=case_sensitive))
146
+ if auto_clean_name:
147
+ cleaned_col_name = clean_name(col, case_sensitive=case_sensitive)
148
+ else:
149
+ cleaned_col_name = col
150
+ unique_constraints_clean.append(cleaned_col_name)
146
151
  unique_constraints_escaped = [f'"{col}"'
147
152
  for col in unique_constraints_clean]
148
153
  index_name = '_'.join([
mage_ai/io/mssql.py CHANGED
@@ -11,10 +11,24 @@ from sqlalchemy.engine import URL
11
11
 
12
12
  from mage_ai.io.base import QUERY_ROW_LIMIT, 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.io.export_utils import PandasTypes
15
16
  from mage_ai.io.sql import BaseSQL
16
17
  from mage_ai.shared.parsers import encode_complex
17
18
 
19
+ MERGE_TABLE_SQL = '''MERGE {table_name} AS t
20
+ USING (VALUES
21
+ ({values_placeholder})
22
+ ) s({columns})
23
+ ON {on_clause}
24
+ WHEN NOT MATCHED THEN
25
+ INSERT ({insert})
26
+ VALUES ({values})
27
+ WHEN MATCHED THEN UPDATE SET
28
+ {update}
29
+ ;
30
+ '''
31
+
18
32
 
19
33
  class MSSQL(BaseSQL):
20
34
  def __init__(
@@ -36,7 +50,7 @@ class MSSQL(BaseSQL):
36
50
  schema=schema,
37
51
  port=port,
38
52
  verbose=verbose,
39
- **kwargs
53
+ **kwargs,
40
54
  )
41
55
 
42
56
  @property
@@ -71,26 +85,29 @@ class MSSQL(BaseSQL):
71
85
  # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794
72
86
  # now struct.unpack: e.g., (2017, 3, 16, 10, 35, 18, 500000000, -6, 0)
73
87
  tup = struct.unpack("<6hI2h", dto_value)
74
- return datetime(tup[0], tup[1], tup[2], tup[3], tup[4], tup[5], tup[6] // 1000,
75
- timezone(timedelta(hours=tup[7], minutes=tup[8])))
88
+ return datetime(
89
+ tup[0],
90
+ tup[1],
91
+ tup[2],
92
+ tup[3],
93
+ tup[4],
94
+ tup[5],
95
+ tup[6] // 1000,
96
+ timezone(timedelta(hours=tup[7], minutes=tup[8])),
97
+ )
76
98
 
77
99
  self._ctx.add_output_converter(-155, handle_datetimeoffset)
78
100
 
79
- def build_create_schema_command(
80
- self,
81
- schema_name: str
82
- ) -> str:
83
- return '\n'.join([
101
+ def build_create_schema_command(self, schema_name: str) -> str:
102
+ return '\n'.join(
103
+ [
84
104
  'IF NOT EXISTS (',
85
105
  f'SELECT * FROM information_schema.schemata WHERE schema_name = \'{schema_name}\')',
86
- f'BEGIN EXEC(\'CREATE SCHEMA {schema_name}\') END'
87
- ])
106
+ f'BEGIN EXEC(\'CREATE SCHEMA {schema_name}\') END',
107
+ ]
108
+ )
88
109
 
89
- def build_create_table_as_command(
90
- self,
91
- table_name: str,
92
- query_string: str
93
- ) -> str:
110
+ def build_create_table_as_command(self, table_name: str, query_string: str) -> str:
94
111
  return 'SELECT * INTO {}\nFROM ({}) AS prev'.format(
95
112
  table_name,
96
113
  query_string,
@@ -98,10 +115,14 @@ class MSSQL(BaseSQL):
98
115
 
99
116
  def table_exists(self, schema_name: str, table_name: str) -> bool:
100
117
  with self.conn.cursor() as cur:
101
- cur.execute('\n'.join([
102
- 'SELECT TOP 1 * FROM information_schema.tables ',
103
- f'WHERE table_schema = \'{schema_name}\' AND table_name = \'{table_name}\'',
104
- ]))
118
+ cur.execute(
119
+ '\n'.join(
120
+ [
121
+ 'SELECT TOP 1 * FROM information_schema.tables ',
122
+ f'WHERE table_schema = \'{schema_name}\' AND table_name = \'{table_name}\'',
123
+ ]
124
+ )
125
+ )
105
126
  return len(cur.fetchall()) >= 1
106
127
 
107
128
  def upload_dataframe(
@@ -146,7 +167,9 @@ class MSSQL(BaseSQL):
146
167
 
147
168
  # Remove extraneous surrounding double quotes
148
169
  # that get added while performing conversion to string.
149
- df_[col] = df_[col].apply(lambda x: x.strip('"') if x and isinstance(x, str) else x)
170
+ df_[col] = df_[col].apply(
171
+ lambda x: x.strip('"') if x and isinstance(x, str) else x
172
+ )
150
173
  df_.replace({np.NaN: None}, inplace=True)
151
174
  for _, row in df_.iterrows():
152
175
  values.append(tuple(row))
@@ -159,8 +182,8 @@ class MSSQL(BaseSQL):
159
182
  df: DataFrame,
160
183
  schema_name: str,
161
184
  table_name: str,
162
- if_exists: ExportWritePolicy = ExportWritePolicy.REPLACE,
163
-
185
+ if_exists: ExportWritePolicy = None,
186
+ **kwargs,
164
187
  ):
165
188
  connection_url = URL.create(
166
189
  'mssql+pyodbc',
@@ -178,7 +201,55 @@ class MSSQL(BaseSQL):
178
201
  connection_url,
179
202
  fast_executemany=True,
180
203
  )
181
- df.to_sql(table_name, engine, schema=schema_name, if_exists=if_exists, index=False)
204
+
205
+ unique_conflict_method = kwargs.get('unique_conflict_method')
206
+ unique_constraints = kwargs.get('unique_constraints')
207
+
208
+ if unique_conflict_method and unique_constraints:
209
+
210
+ def merge_table(
211
+ table, conn, keys, data_iter, unique_constraints=unique_constraints
212
+ ):
213
+ dbapi_conn = conn.connection
214
+ with dbapi_conn.cursor() as cur:
215
+ if table.schema:
216
+ table_name = f'[{table.schema}].[{table.name}]'
217
+ else:
218
+ table_name = table.name
219
+
220
+ values_placeholder = ', '.join(['?' for i in range(len(keys))])
221
+ values = [tuple(row) for row in data_iter]
222
+ sql = MERGE_TABLE_SQL.format(
223
+ table_name=table_name,
224
+ values_placeholder=values_placeholder,
225
+ columns=', '.join([f'[{k}]' for k in keys]),
226
+ on_clause=' AND '.join(
227
+ [f's.[{k}] = t.[{k}]' for k in unique_constraints]
228
+ ),
229
+ insert=', '.join([f'[{c}]' for c in keys]),
230
+ values=', '.join([f's.[{c}]' for c in keys]),
231
+ update=', '.join([f'[{c}] = s.[{c}]' for c in keys]),
232
+ )
233
+ cur.executemany(sql, values)
234
+
235
+ if UNIQUE_CONFLICT_METHOD_UPDATE == unique_conflict_method:
236
+ df.to_sql(
237
+ table_name,
238
+ engine,
239
+ schema=schema_name,
240
+ if_exists=if_exists or ExportWritePolicy.APPEND,
241
+ index=False,
242
+ method=merge_table,
243
+ )
244
+ return
245
+
246
+ df.to_sql(
247
+ table_name,
248
+ engine,
249
+ schema=schema_name,
250
+ if_exists=if_exists or ExportWritePolicy.REPLACE,
251
+ index=False,
252
+ )
182
253
 
183
254
  def get_type(self, column: Series, dtype: str) -> str:
184
255
  if dtype in (
@@ -210,7 +281,11 @@ class MSSQL(BaseSQL):
210
281
  return 'text'
211
282
  elif dtype == PandasTypes.BYTES:
212
283
  return 'varbinary(255)'
213
- elif dtype in (PandasTypes.FLOATING, PandasTypes.DECIMAL, PandasTypes.MIXED_INTEGER_FLOAT):
284
+ elif dtype in (
285
+ PandasTypes.FLOATING,
286
+ PandasTypes.DECIMAL,
287
+ PandasTypes.MIXED_INTEGER_FLOAT,
288
+ ):
214
289
  return 'decimal'
215
290
  elif dtype == PandasTypes.INTEGER:
216
291
  max_int, min_int = column.max(), column.min()
@@ -222,7 +297,11 @@ class MSSQL(BaseSQL):
222
297
  return 'bigint'
223
298
  elif dtype == PandasTypes.BOOLEAN:
224
299
  return 'char(52)'
225
- elif dtype in (PandasTypes.TIMEDELTA, PandasTypes.TIMEDELTA64, PandasTypes.PERIOD):
300
+ elif dtype in (
301
+ PandasTypes.TIMEDELTA,
302
+ PandasTypes.TIMEDELTA64,
303
+ PandasTypes.PERIOD,
304
+ ):
226
305
  return 'bigint'
227
306
  elif dtype == PandasTypes.EMPTY:
228
307
  return 'char(255)'
mage_ai/io/mysql.py CHANGED
@@ -52,6 +52,8 @@ class MySQL(BaseSQL):
52
52
  dtypes: Mapping[str, str],
53
53
  schema_name: str,
54
54
  table_name: str,
55
+ auto_clean_name: bool = True,
56
+ case_sensitive: bool = False,
55
57
  unique_constraints: List[str] = None,
56
58
  overwrite_types: Dict = None,
57
59
  **kwargs,
@@ -59,15 +61,14 @@ class MySQL(BaseSQL):
59
61
  if unique_constraints is None:
60
62
  unique_constraints = []
61
63
  query = []
62
- if overwrite_types is not None:
63
- for cname in dtypes:
64
- if cname in overwrite_types.keys():
65
- dtypes[cname] = overwrite_types[cname]
66
- query.append(f'`{clean_name(cname)}` {dtypes[cname]} NULL')
67
-
68
- else:
69
- for cname in dtypes:
70
- query.append(f'`{clean_name(cname)}` {dtypes[cname]} NULL')
64
+ for cname in dtypes:
65
+ if overwrite_types is not None and cname in overwrite_types.keys():
66
+ dtypes[cname] = overwrite_types[cname]
67
+ if auto_clean_name:
68
+ cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
69
+ else:
70
+ cleaned_col_name = cname
71
+ query.append(f'`{cleaned_col_name}` {dtypes[cname]} NULL')
71
72
 
72
73
  return f'CREATE TABLE {table_name} (' + ','.join(query) + ');'
73
74
 
mage_ai/io/oracledb.py CHANGED
@@ -7,6 +7,9 @@ from pandas import DataFrame, read_sql
7
7
  from mage_ai.io.base import QUERY_ROW_LIMIT
8
8
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
9
9
  from mage_ai.io.sql import BaseSQL
10
+ from mage_ai.server.logger import Logger
11
+
12
+ logger = Logger().new_server_logger(__name__)
10
13
 
11
14
 
12
15
  class OracleDB(BaseSQL):
@@ -40,8 +43,17 @@ class OracleDB(BaseSQL):
40
43
  )
41
44
 
42
45
  def open(self) -> None:
43
- with self.printer.print_msg('Opening connection to OracleDB database (thin mode)'):
44
- self._ctx = oracledb.connect(**self.settings)
46
+ if self.settings['mode'] and self.settings['mode'].lower() == 'thick':
47
+ logger.info('Initializing Oracle thick mode.')
48
+ oracledb.init_oracle_client()
49
+ with self.printer.print_msg(f'Opening connection to OracleDB database \
50
+ ({self.settings["mode"]} mode)'):
51
+ connection_dsn = "{}:{}/{}".format(
52
+ self.settings['host'],
53
+ self.settings['port'],
54
+ self.settings['service_name'])
55
+ self._ctx = oracledb.connect(
56
+ user=self.settings['user'], password=self.settings['password'], dsn=connection_dsn)
45
57
 
46
58
  def load(
47
59
  self,
mage_ai/io/postgres.py CHANGED
@@ -284,6 +284,7 @@ class Postgres(BaseSQL):
284
284
  dtypes: List[str],
285
285
  full_table_name: str,
286
286
  allow_reserved_words: bool = False,
287
+ auto_clean_name: bool = True,
287
288
  buffer: Union[IO, None] = None,
288
289
  case_sensitive: bool = False,
289
290
  unique_conflict_method: str = None,
@@ -355,6 +356,7 @@ class Postgres(BaseSQL):
355
356
  cleaned_col = self._clean_column_name(
356
357
  col,
357
358
  allow_reserved_words=allow_reserved_words,
359
+ auto_clean_name=auto_clean_name,
358
360
  case_sensitive=case_sensitive,
359
361
  )
360
362
  cleaned_unique_constraints.append(f'"{cleaned_col}"')
@@ -364,6 +366,7 @@ class Postgres(BaseSQL):
364
366
  cleaned_col = self._clean_column_name(
365
367
  col,
366
368
  allow_reserved_words=allow_reserved_words,
369
+ auto_clean_name=auto_clean_name,
367
370
  case_sensitive=case_sensitive,
368
371
  )
369
372
  cleaned_columns.append(f'"{cleaned_col}"')
mage_ai/io/sql.py CHANGED
@@ -52,6 +52,7 @@ class BaseSQL(BaseSQLConnection):
52
52
  dtypes: Mapping[str, str],
53
53
  schema_name: str,
54
54
  table_name: str,
55
+ auto_clean_name: bool = True,
55
56
  case_sensitive: bool = False,
56
57
  unique_constraints: List[str] = None,
57
58
  overwrite_types: Dict = None,
@@ -63,6 +64,7 @@ class BaseSQL(BaseSQLConnection):
63
64
  dtypes,
64
65
  schema_name,
65
66
  table_name,
67
+ auto_clean_name=auto_clean_name,
66
68
  case_sensitive=case_sensitive,
67
69
  unique_constraints=unique_constraints,
68
70
  overwrite_types=overwrite_types,
@@ -224,6 +226,7 @@ class BaseSQL(BaseSQLConnection):
224
226
  verbose: bool = True,
225
227
  # Other optional configs
226
228
  allow_reserved_words: bool = False,
229
+ auto_clean_name: bool = True,
227
230
  case_sensitive: bool = False,
228
231
  cascade_on_drop: bool = False,
229
232
  drop_table_on_replace: bool = False,
@@ -276,12 +279,13 @@ class BaseSQL(BaseSQLConnection):
276
279
  df = clean_df_for_export(df, self.clean, dtypes)
277
280
 
278
281
  # Clean column names
279
- col_mapping = {col: self._clean_column_name(
280
- col,
281
- allow_reserved_words=allow_reserved_words,
282
- case_sensitive=case_sensitive)
283
- for col in df.columns}
284
- df = df.rename(columns=col_mapping)
282
+ if auto_clean_name:
283
+ col_mapping = {col: self._clean_column_name(
284
+ col,
285
+ allow_reserved_words=allow_reserved_words,
286
+ case_sensitive=case_sensitive)
287
+ for col in df.columns}
288
+ df = df.rename(columns=col_mapping)
285
289
  dtypes = infer_dtypes(df)
286
290
 
287
291
  def __process():
@@ -292,6 +296,8 @@ class BaseSQL(BaseSQLConnection):
292
296
  schema_name,
293
297
  table_name,
294
298
  if_exists=if_exists,
299
+ unique_conflict_method=unique_conflict_method,
300
+ unique_constraints=unique_constraints,
295
301
  )
296
302
  return
297
303
 
@@ -340,6 +346,7 @@ class BaseSQL(BaseSQLConnection):
340
346
  db_dtypes,
341
347
  schema_name,
342
348
  table_name,
349
+ auto_clean_name=auto_clean_name,
343
350
  case_sensitive=case_sensitive,
344
351
  unique_constraints=unique_constraints,
345
352
  overwrite_types=overwrite_types,
@@ -354,6 +361,7 @@ class BaseSQL(BaseSQLConnection):
354
361
  allow_reserved_words=allow_reserved_words,
355
362
  buffer=buffer,
356
363
  case_sensitive=case_sensitive,
364
+ auto_clean_name=auto_clean_name,
357
365
  unique_conflict_method=unique_conflict_method,
358
366
  unique_constraints=unique_constraints,
359
367
  **kwargs,
mage_ai/io/trino.py CHANGED
@@ -119,6 +119,8 @@ class Trino(BaseSQL):
119
119
  dtypes: Mapping[str, str],
120
120
  schema_name: str,
121
121
  table_name: str,
122
+ auto_clean_name: bool = True,
123
+ case_sensitive: bool = False,
122
124
  unique_constraints: List[str] = None,
123
125
  overwrite_types: Dict = None,
124
126
  **kwargs,
@@ -126,14 +128,14 @@ class Trino(BaseSQL):
126
128
  if unique_constraints is None:
127
129
  unique_constraints = []
128
130
  query = []
129
- if overwrite_types is not None:
130
- for cname in dtypes:
131
- if cname in overwrite_types.keys():
132
- dtypes[cname] = overwrite_types[cname]
133
- query.append(f'"{clean_name(cname)}" {dtypes[cname]}')
134
- else:
135
- for cname in dtypes:
136
- query.append(f'"{clean_name(cname)}" {dtypes[cname]}')
131
+ for cname in dtypes:
132
+ if overwrite_types is not None and cname in overwrite_types.keys():
133
+ dtypes[cname] = overwrite_types[cname]
134
+ if auto_clean_name:
135
+ cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
136
+ else:
137
+ cleaned_col_name = cname
138
+ query.append(f'"{cleaned_col_name}" {dtypes[cname]}')
137
139
 
138
140
  full_table_name = '.'.join(list(filter(lambda x: x, [
139
141
  schema_name,
@@ -20,11 +20,17 @@ def upgrade() -> None:
20
20
  # ### commands auto generated by Alembic - please adjust! ###
21
21
  try:
22
22
  bind = op.get_bind()
23
- insp = sa.inspect(bind.engine)
24
- with op.batch_alter_table('secret', schema=None) as batch_op:
25
- unique_constraints = insp.get_unique_constraints('secret')
26
- for constraint in unique_constraints:
27
- batch_op.drop_constraint(constraint['name'], type_='unique')
23
+ insp = sa.inspect(bind)
24
+ if bind.engine.name == 'mssql':
25
+ indexes = insp.get_indexes('secret')
26
+ for i in indexes:
27
+ if i['unique'] is True:
28
+ op.execute(f"ALTER TABLE secret DROP CONSTRAINT {i['name']};")
29
+ else:
30
+ with op.batch_alter_table('secret', schema=None) as batch_op:
31
+ unique_constraints = insp.get_unique_constraints('secret')
32
+ for constraint in unique_constraints:
33
+ batch_op.drop_constraint(constraint['name'], type_='unique')
28
34
  except ValueError:
29
35
  with op.batch_alter_table('secret', schema=None) as batch_op:
30
36
  batch_op.add_column(sa.Column('name_new', sa.String(length=255), nullable=True))
@@ -337,6 +337,7 @@ class PipelineSchedule(PipelineScheduleProjectPlatformMixin, BaseModel):
337
337
  last_enabled_at=last_enabled_at,
338
338
  name=trigger_config.name,
339
339
  pipeline_uuid=trigger_config.pipeline_uuid,
340
+ repo_path=trigger_config.repo_path,
340
341
  schedule_interval=trigger_config.schedule_interval,
341
342
  schedule_type=trigger_config.schedule_type,
342
343
  settings=trigger_config.settings,
@@ -351,6 +352,7 @@ class PipelineSchedule(PipelineScheduleProjectPlatformMixin, BaseModel):
351
352
  existing_trigger.last_enabled_at != kwargs.get('last_enabled_at'),
352
353
  existing_trigger.name != kwargs.get('name'),
353
354
  existing_trigger.pipeline_uuid != kwargs.get('pipeline_uuid'),
355
+ existing_trigger.repo_path != kwargs.get('repo_path'),
354
356
  existing_trigger.schedule_interval != kwargs.get('schedule_interval'),
355
357
  existing_trigger.schedule_type != kwargs.get('schedule_type'),
356
358
  existing_trigger.settings != kwargs.get('settings'),
@@ -1548,7 +1550,12 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
1548
1550
  interval_start_datetime = self.execution_date
1549
1551
  interval_start_datetime_previous = None
1550
1552
 
1551
- if ScheduleInterval.DAILY == self.pipeline_schedule.schedule_interval:
1553
+ if (
1554
+ ScheduleInterval.ONCE == self.pipeline_schedule.schedule_interval
1555
+ or ScheduleInterval.ALWAYS_ON == self.pipeline_schedule.schedule_interval
1556
+ ):
1557
+ pass
1558
+ elif ScheduleInterval.DAILY == self.pipeline_schedule.schedule_interval:
1552
1559
  interval_seconds = 60 * 60 * 24
1553
1560
  elif ScheduleInterval.HOURLY == self.pipeline_schedule.schedule_interval:
1554
1561
  interval_seconds = 60 * 60 * 1
@@ -1559,6 +1566,23 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
1559
1566
  )
1560
1567
  elif ScheduleInterval.WEEKLY == self.pipeline_schedule.schedule_interval:
1561
1568
  interval_seconds = 60 * 60 * 24 * 7
1569
+ else:
1570
+ try:
1571
+ cron_itr = croniter(
1572
+ self.pipeline_schedule.schedule_interval,
1573
+ self.execution_date,
1574
+ )
1575
+ current = cron_itr.get_current(datetime)
1576
+ interval_start_datetime_previous = cron_itr.get_prev(datetime)
1577
+ # get_prev and get_next changes the state of the cron iterator, so we need
1578
+ # to call get_next again to go back to the original state
1579
+ cron_itr.get_next()
1580
+ interval_end_datetime = cron_itr.get_next(datetime)
1581
+ interval_seconds = (
1582
+ interval_end_datetime.timestamp() - current.timestamp()
1583
+ )
1584
+ except Exception:
1585
+ pass
1562
1586
 
1563
1587
  if interval_seconds and not interval_end_datetime:
1564
1588
  interval_end_datetime = interval_start_datetime + timedelta(
@@ -5,6 +5,7 @@ from typing import Dict, List
5
5
 
6
6
  import dateutil.parser
7
7
  import pytz
8
+ from croniter import croniter
8
9
  from dateutil.relativedelta import relativedelta
9
10
  from sqlalchemy import or_
10
11
  from sqlalchemy.sql import func
@@ -298,7 +299,12 @@ class PipelineRunProjectPlatformMixin:
298
299
  interval_start_datetime = self.execution_date
299
300
  interval_start_datetime_previous = None
300
301
 
301
- if ScheduleInterval.DAILY == self.pipeline_schedule.schedule_interval:
302
+ if (
303
+ ScheduleInterval.ONCE == self.pipeline_schedule.schedule_interval
304
+ or ScheduleInterval.ALWAYS_ON == self.pipeline_schedule.schedule_interval
305
+ ):
306
+ pass
307
+ elif ScheduleInterval.DAILY == self.pipeline_schedule.schedule_interval:
302
308
  interval_seconds = 60 * 60 * 24
303
309
  elif ScheduleInterval.HOURLY == self.pipeline_schedule.schedule_interval:
304
310
  interval_seconds = 60 * 60 * 1
@@ -309,6 +315,23 @@ class PipelineRunProjectPlatformMixin:
309
315
  )
310
316
  elif ScheduleInterval.WEEKLY == self.pipeline_schedule.schedule_interval:
311
317
  interval_seconds = 60 * 60 * 24 * 7
318
+ else:
319
+ try:
320
+ cron_itr = croniter(
321
+ self.pipeline_schedule.schedule_interval,
322
+ self.execution_date,
323
+ )
324
+ current = cron_itr.get_current(datetime)
325
+ interval_start_datetime_previous = cron_itr.get_prev(datetime)
326
+ # get_prev and get_next changes the state of the cron iterator, so we need
327
+ # to call get_next again to go back to the original state
328
+ cron_itr.get_next()
329
+ interval_end_datetime = cron_itr.get_next(datetime)
330
+ interval_seconds = (
331
+ interval_end_datetime.timestamp() - current.timestamp()
332
+ )
333
+ except Exception:
334
+ pass
312
335
 
313
336
  if interval_seconds and not interval_end_datetime:
314
337
  interval_end_datetime = interval_start_datetime + timedelta(
@@ -1,3 +1,4 @@
1
+ import traceback
1
2
  from enum import Enum
2
3
  from typing import Callable, Dict, Union
3
4
 
@@ -89,4 +90,8 @@ class JobManager:
89
90
  return f'{job_type}_{uid}'
90
91
 
91
92
 
92
- job_manager = JobManager()
93
+ try:
94
+ job_manager = JobManager()
95
+ except Exception:
96
+ traceback.print_exc()
97
+ job_manager = None
@@ -240,19 +240,14 @@ class PipelineScheduler:
240
240
  failed_block_runs = self.pipeline_run.failed_block_runs
241
241
  error_msg = 'Failed blocks: '\
242
242
  f'{", ".join([b.block_uuid for b in failed_block_runs])}.'
243
- self.notification_sender.send_pipeline_run_failure_message(
244
- error=error_msg,
245
- pipeline=self.pipeline,
246
- pipeline_run=self.pipeline_run,
247
- )
243
+ self.on_pipeline_run_failure(error_msg)
248
244
  else:
249
245
  self.pipeline_run.complete()
250
246
  self.notification_sender.send_pipeline_run_success_message(
251
247
  pipeline=self.pipeline,
252
248
  pipeline_run=self.pipeline_run,
253
249
  )
254
-
255
- asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
250
+ asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
256
251
 
257
252
  self.logger_manager.output_logs_to_destination()
258
253
 
@@ -323,12 +318,20 @@ class PipelineScheduler:
323
318
  self.__schedule_blocks(block_runs)
324
319
 
325
320
  @safe_db_query
326
- def on_pipeline_run_failure(self, error: str) -> None:
321
+ def on_pipeline_run_failure(self, error_msg: str) -> None:
322
+ failed_block_runs = self.pipeline_run.failed_block_runs
323
+ for br in failed_block_runs:
324
+ if br.metrics:
325
+ message = br.metrics.get('error', {}).get('message')
326
+ if message:
327
+ error_msg += f'\nError for block {br.block_uuid}:\n{message}'
328
+ break
329
+
327
330
  asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
328
331
  self.notification_sender.send_pipeline_run_failure_message(
329
332
  pipeline=self.pipeline,
330
333
  pipeline_run=self.pipeline_run,
331
- error=error,
334
+ error=error_msg,
332
335
  )
333
336
  # Cancel block runs that are still in progress for the pipeline run.
334
337
  cancel_block_runs_and_jobs(self.pipeline_run, self.pipeline)
@@ -1422,7 +1425,10 @@ def schedule_all():
1422
1425
  ))
1423
1426
 
1424
1427
  # Sync schedules from yaml file to DB
1425
- sync_schedules(list(repo_pipelines))
1428
+ try:
1429
+ sync_schedules(list(repo_pipelines))
1430
+ except Exception:
1431
+ logger.exception('Failed to sync schedules')
1426
1432
 
1427
1433
  active_pipeline_schedules = list(PipelineSchedule.active_schedules(
1428
1434
  pipeline_uuids=repo_pipelines,
@@ -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.65'
15
+ '0.9.67'
@@ -0,0 +1,10 @@
1
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
2
+
3
+ from mage_ai.data_preparation.repo_manager import update_settings_on_metadata_change
4
+
5
+
6
+ class MetadataEventHandler(FileSystemEventHandler):
7
+ def on_modified(self, event: FileSystemEvent):
8
+ super().on_modified(event)
9
+
10
+ update_settings_on_metadata_change()