mage-ai 0.9.68__py3-none-any.whl → 0.9.70__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 (316) hide show
  1. mage_ai/api/policies/BackfillPolicy.py +1 -0
  2. mage_ai/api/policies/PipelinePolicy.py +1 -0
  3. mage_ai/api/policies/WorkspacePolicy.py +1 -0
  4. mage_ai/api/presenters/BackfillPresenter.py +1 -0
  5. mage_ai/api/resources/GitBranchResource.py +56 -23
  6. mage_ai/api/resources/GitCustomBranchResource.py +29 -1
  7. mage_ai/api/resources/OauthResource.py +1 -1
  8. mage_ai/api/resources/PipelineResource.py +11 -5
  9. mage_ai/api/resources/PipelineRunResource.py +41 -4
  10. mage_ai/api/resources/PipelineScheduleResource.py +4 -0
  11. mage_ai/api/resources/PullRequestResource.py +6 -4
  12. mage_ai/api/resources/SeedResource.py +2 -1
  13. mage_ai/api/resources/SessionResource.py +13 -1
  14. mage_ai/api/resources/WorkspaceResource.py +5 -4
  15. mage_ai/authentication/permissions/constants.py +2 -0
  16. mage_ai/authentication/permissions/seed.py +32 -21
  17. mage_ai/authentication/providers/active_directory.py +4 -3
  18. mage_ai/authentication/providers/okta.py +22 -83
  19. mage_ai/cache/block_action_object/__init__.py +1 -1
  20. mage_ai/cluster_manager/kubernetes/workload_manager.py +52 -1
  21. mage_ai/cluster_manager/workspace/base.py +6 -0
  22. mage_ai/cluster_manager/workspace/kubernetes.py +22 -1
  23. mage_ai/command_center/applications/utils.py +2 -2
  24. mage_ai/command_center/presenters/text.py +1 -1
  25. mage_ai/data_preparation/executors/k8s_block_executor.py +30 -7
  26. mage_ai/data_preparation/executors/k8s_pipeline_executor.py +30 -7
  27. mage_ai/data_preparation/executors/streaming_pipeline_executor.py +78 -8
  28. mage_ai/data_preparation/git/__init__.py +50 -22
  29. mage_ai/data_preparation/git/api.py +62 -7
  30. mage_ai/data_preparation/git/utils.py +45 -21
  31. mage_ai/data_preparation/models/block/__init__.py +31 -8
  32. mage_ai/data_preparation/models/block/data_integration/mixins.py +16 -5
  33. mage_ai/data_preparation/models/block/dynamic/child.py +3 -0
  34. mage_ai/data_preparation/models/block/dynamic/utils.py +9 -4
  35. mage_ai/data_preparation/models/block/dynamic/variables.py +2 -2
  36. mage_ai/data_preparation/models/block/extension/utils.py +1 -0
  37. mage_ai/data_preparation/models/block/global_data_product/__init__.py +25 -2
  38. mage_ai/data_preparation/models/block/integration/__init__.py +1 -1
  39. mage_ai/data_preparation/models/block/remote/__init__.py +0 -0
  40. mage_ai/data_preparation/models/block/remote/models.py +58 -0
  41. mage_ai/data_preparation/models/block/sql/__init__.py +1 -1
  42. mage_ai/data_preparation/models/block/utils.py +38 -0
  43. mage_ai/data_preparation/models/constants.py +2 -0
  44. mage_ai/data_preparation/models/global_data_product/__init__.py +12 -0
  45. mage_ai/data_preparation/models/pipeline.py +31 -11
  46. mage_ai/data_preparation/models/triggers/__init__.py +4 -2
  47. mage_ai/data_preparation/models/utils.py +6 -0
  48. mage_ai/data_preparation/models/variable.py +18 -4
  49. mage_ai/data_preparation/repo_manager.py +3 -2
  50. mage_ai/data_preparation/shared/utils.py +1 -1
  51. mage_ai/data_preparation/storage/local_storage.py +12 -6
  52. mage_ai/data_preparation/templates/data_exporters/mysql.py +2 -2
  53. mage_ai/data_preparation/templates/data_exporters/oracledb.py +27 -0
  54. mage_ai/data_preparation/templates/repo/metadata.yaml +1 -0
  55. mage_ai/io/bigquery.py +131 -58
  56. mage_ai/io/mysql.py +38 -6
  57. mage_ai/io/snowflake.py +152 -29
  58. mage_ai/orchestration/db/migrations/versions/42a14d6143f1_update_token_column_type.py +54 -0
  59. mage_ai/orchestration/db/models/oauth.py +14 -13
  60. mage_ai/orchestration/db/models/schedules.py +30 -2
  61. mage_ai/orchestration/job_manager.py +6 -0
  62. mage_ai/orchestration/notification/sender.py +37 -15
  63. mage_ai/orchestration/pipeline_scheduler_original.py +48 -31
  64. mage_ai/orchestration/queue/celery_queue.py +8 -1
  65. mage_ai/orchestration/queue/process_queue.py +67 -4
  66. mage_ai/orchestration/queue/queue.py +8 -0
  67. mage_ai/orchestration/triggers/api.py +29 -1
  68. mage_ai/orchestration/triggers/global_data_product.py +9 -4
  69. mage_ai/orchestration/triggers/utils.py +10 -1
  70. mage_ai/orchestration/utils/resources.py +3 -0
  71. mage_ai/server/api/downloads.py +4 -1
  72. mage_ai/server/api/runs.py +151 -0
  73. mage_ai/server/constants.py +1 -1
  74. mage_ai/server/frontend_dist/404.html +6 -6
  75. mage_ai/server/frontend_dist/_next/static/{i8pymuJDTVHdWjUP1QSh1 → RhDiJSkcjCsh4xxX4BFBk}/_buildManifest.js +1 -1
  76. mage_ai/server/frontend_dist/_next/static/chunks/1557-b3502f3f1aa92ac7.js +1 -0
  77. mage_ai/server/frontend_dist/_next/static/chunks/2717-d9200be634dd6766.js +1 -0
  78. mage_ai/server/frontend_dist/_next/static/chunks/3548-9d26185b3fb663b1.js +1 -0
  79. mage_ai/server/frontend_dist/_next/static/chunks/5699-6d708c6b2153ea08.js +1 -0
  80. mage_ai/server/frontend_dist/_next/static/chunks/7361-8a23dd8360593e7a.js +1 -0
  81. mage_ai/server/frontend_dist/_next/static/chunks/7966-b9b85ba10667e654.js +1 -0
  82. mage_ai/server/frontend_dist/_next/static/chunks/9624-8b8e100079ab69e1.js +1 -0
  83. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-2a69553d8c6eeb53.js +1 -0
  84. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-4e12783b064c1cfe.js +1 -0
  85. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-4bfc84ff07d7656f.js +1 -0
  86. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipeline-runs-a66b4c7641ae03eb.js → frontend_dist/_next/static/chunks/pages/pipeline-runs-6d183f91a2ff6668.js} +1 -1
  87. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-7181b086c93784d2.js +1 -0
  88. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-38e1fbcfbfc1014e.js +1 -0
  89. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-b645a6d13ab9fe3a.js +1 -0
  90. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-59aca25a5b1d3998.js +1 -0
  91. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-eb11c5390c982b49.js +1 -0
  92. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-cb88fd075a357fcf.js → triggers-4612d15a65c35912.js} +1 -1
  93. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-3591d035bb3bb2b8.js +1 -0
  94. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-32985f3f7c7dd3ab.js +1 -0
  95. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-c2e9ef989c8bfa73.js +1 -0
  96. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-349af617d05f001b.js +1 -0
  97. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/sync-data-60d01d3887e31136.js +1 -0
  98. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{users-86814e581acaf5db.js → users-a4db8710f703c729.js} +1 -1
  99. mage_ai/server/frontend_dist/_next/static/chunks/pages/sign-in-09414a8b66fb6f06.js +1 -0
  100. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/triggers-9cba3211434a8966.js → frontend_dist/_next/static/chunks/pages/triggers-a599c6ac89be8c8d.js} +1 -1
  101. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-3433c8b22e8342aa.js +1 -0
  102. mage_ai/server/frontend_dist/block-layout.html +2 -2
  103. mage_ai/server/frontend_dist/compute.html +2 -2
  104. mage_ai/server/frontend_dist/files.html +2 -2
  105. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  106. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  107. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  108. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  109. mage_ai/server/frontend_dist/index.html +2 -2
  110. mage_ai/server/frontend_dist/manage/files.html +2 -2
  111. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  112. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  113. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  114. mage_ai/server/frontend_dist/manage/users.html +2 -2
  115. mage_ai/server/frontend_dist/manage.html +2 -2
  116. mage_ai/server/frontend_dist/oauth.html +2 -2
  117. mage_ai/server/frontend_dist/overview.html +2 -2
  118. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  119. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  120. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  121. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  122. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  123. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  124. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  125. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  126. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  127. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  128. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  129. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  130. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  131. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  132. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  133. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  134. mage_ai/server/frontend_dist/pipelines.html +2 -2
  135. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
  136. mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
  137. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  138. mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
  139. mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
  140. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  141. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  142. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  143. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  144. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  145. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  146. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  147. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  148. mage_ai/server/frontend_dist/settings.html +2 -2
  149. mage_ai/server/frontend_dist/sign-in.html +6 -6
  150. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  151. mage_ai/server/frontend_dist/templates.html +2 -2
  152. mage_ai/server/frontend_dist/terminal.html +2 -2
  153. mage_ai/server/frontend_dist/test.html +2 -2
  154. mage_ai/server/frontend_dist/triggers.html +2 -2
  155. mage_ai/server/frontend_dist/version-control.html +2 -2
  156. mage_ai/server/frontend_dist_base_path_template/404.html +6 -6
  157. mage_ai/server/frontend_dist_base_path_template/_next/static/{CKCvjsYCf2imD2X8zAOBf → TdpLLFome13qvM0gXvpHs}/_buildManifest.js +1 -1
  158. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-b3502f3f1aa92ac7.js +1 -0
  159. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-d9200be634dd6766.js +1 -0
  160. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-9d26185b3fb663b1.js +1 -0
  161. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5699-6d708c6b2153ea08.js +1 -0
  162. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-8a23dd8360593e7a.js +1 -0
  163. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7966-b9b85ba10667e654.js +1 -0
  164. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9624-8b8e100079ab69e1.js +1 -0
  165. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-2a69553d8c6eeb53.js +1 -0
  166. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-4e12783b064c1cfe.js +1 -0
  167. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage-4bfc84ff07d7656f.js +1 -0
  168. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipeline-runs-a66b4c7641ae03eb.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipeline-runs-6d183f91a2ff6668.js} +1 -1
  169. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-7181b086c93784d2.js +1 -0
  170. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-38e1fbcfbfc1014e.js +1 -0
  171. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-b645a6d13ab9fe3a.js +1 -0
  172. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-59aca25a5b1d3998.js +1 -0
  173. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-eb11c5390c982b49.js +1 -0
  174. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-cb88fd075a357fcf.js → triggers-4612d15a65c35912.js} +1 -1
  175. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-3591d035bb3bb2b8.js +1 -0
  176. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-32985f3f7c7dd3ab.js +1 -0
  177. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-c2e9ef989c8bfa73.js +1 -0
  178. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-349af617d05f001b.js +1 -0
  179. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/sync-data-60d01d3887e31136.js +1 -0
  180. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{users-86814e581acaf5db.js → users-a4db8710f703c729.js} +1 -1
  181. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/sign-in-09414a8b66fb6f06.js +1 -0
  182. mage_ai/server/{frontend_dist/_next/static/chunks/pages/triggers-9cba3211434a8966.js → frontend_dist_base_path_template/_next/static/chunks/pages/triggers-a599c6ac89be8c8d.js} +1 -1
  183. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-3433c8b22e8342aa.js +1 -0
  184. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  185. mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
  186. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  187. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  188. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  189. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
  190. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
  191. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  192. mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
  193. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  194. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  195. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  196. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  197. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  198. mage_ai/server/frontend_dist_base_path_template/oauth.html +2 -2
  199. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  200. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  201. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  202. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  203. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
  204. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  205. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  206. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  207. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  208. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  209. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  210. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  211. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  212. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  213. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  214. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  215. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  216. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  217. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
  218. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
  219. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  220. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
  221. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
  222. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
  223. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
  224. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  225. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
  226. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
  227. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  228. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
  229. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  230. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  231. mage_ai/server/frontend_dist_base_path_template/sign-in.html +6 -6
  232. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  233. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  234. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  235. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  236. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  237. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  238. mage_ai/server/scheduler_manager.py +9 -0
  239. mage_ai/server/server.py +47 -17
  240. mage_ai/server/utils/output_display.py +2 -2
  241. mage_ai/server/websocket_server.py +1 -0
  242. mage_ai/services/aws/ecs/ecs.py +1 -0
  243. mage_ai/services/k8s/config.py +4 -4
  244. mage_ai/services/k8s/utils.py +97 -0
  245. mage_ai/settings/keys/auth.py +2 -0
  246. mage_ai/settings/server.py +1 -1
  247. mage_ai/shared/parsers.py +6 -1
  248. mage_ai/streaming/sources/influxdb.py +2 -0
  249. mage_ai/streaming/sources/kafka.py +1 -1
  250. mage_ai/tests/api/endpoints/mixins.py +10 -9
  251. mage_ai/tests/api/endpoints/test_seeds.py +24 -0
  252. mage_ai/tests/api/operations/base/mixins.py +1 -1
  253. mage_ai/tests/api/operations/test_sessions.py +53 -2
  254. mage_ai/tests/api/resources/test_pipeline_resource.py +2 -2
  255. mage_ai/tests/authentication/oauth/test_utils.py +1 -1
  256. mage_ai/tests/authentication/providers/test_okta.py +43 -0
  257. mage_ai/tests/data_preparation/models/block/test_global_data_product.py +2 -0
  258. mage_ai/tests/orchestration/db/models/test_oauth.py +3 -3
  259. mage_ai/tests/orchestration/queue/test_process_queue.py +1 -0
  260. mage_ai/tests/orchestration/triggers/test_global_data_product.py +138 -136
  261. mage_ai/tests/server/test_server.py +27 -4
  262. mage_ai/tests/services/k8s/test_job_manager.py +9 -6
  263. mage_ai/version_control/branch/utils.py +2 -1
  264. mage_ai/version_control/models.py +3 -2
  265. {mage_ai-0.9.68.dist-info → mage_ai-0.9.70.dist-info}/METADATA +5 -5
  266. {mage_ai-0.9.68.dist-info → mage_ai-0.9.70.dist-info}/RECORD +272 -264
  267. mage_ai/server/frontend_dist/_next/static/chunks/1557-01f0843dc6ac4971.js +0 -1
  268. mage_ai/server/frontend_dist/_next/static/chunks/2717-b5f9575799b594d5.js +0 -1
  269. mage_ai/server/frontend_dist/_next/static/chunks/3548-961ff79ca70038c7.js +0 -1
  270. mage_ai/server/frontend_dist/_next/static/chunks/5699-6efc749f2f8ddd20.js +0 -1
  271. mage_ai/server/frontend_dist/_next/static/chunks/7361-18d9d8be96e1ce97.js +0 -1
  272. mage_ai/server/frontend_dist/_next/static/chunks/7966-f07b2913f7326b50.js +0 -1
  273. mage_ai/server/frontend_dist/_next/static/chunks/9624-59b2f803f9c88cd6.js +0 -1
  274. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-08790743315de36a.js +0 -1
  275. mage_ai/server/frontend_dist/_next/static/chunks/pages/index-13760bb72d823b69.js +0 -1
  276. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-852d403c7bda21b3.js +0 -1
  277. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-ff4bd7a8ec3bab40.js +0 -1
  278. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-a8b61d8d239fd16f.js +0 -1
  279. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-bd0aff5a5ed8888c.js +0 -1
  280. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-d1ee961387c58b7f.js +0 -1
  281. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-f028ef3880ed856c.js +0 -1
  282. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-ceb06e1616ee9610.js +0 -1
  283. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-8ff16ef9566e911a.js +0 -1
  284. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/settings-74d76300942dcee8.js +0 -1
  285. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-d7a8bc51bb7a1082.js +0 -1
  286. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/sync-data-79a4cf66a623e667.js +0 -1
  287. mage_ai/server/frontend_dist/_next/static/chunks/pages/sign-in-f59d34429fe022ee.js +0 -1
  288. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-5753fac7c1bfdc88.js +0 -1
  289. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-01f0843dc6ac4971.js +0 -1
  290. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-b5f9575799b594d5.js +0 -1
  291. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-961ff79ca70038c7.js +0 -1
  292. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/5699-6efc749f2f8ddd20.js +0 -1
  293. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-18d9d8be96e1ce97.js +0 -1
  294. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7966-f07b2913f7326b50.js +0 -1
  295. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9624-59b2f803f9c88cd6.js +0 -1
  296. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-08790743315de36a.js +0 -1
  297. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-13760bb72d823b69.js +0 -1
  298. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage-852d403c7bda21b3.js +0 -1
  299. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-ff4bd7a8ec3bab40.js +0 -1
  300. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-a8b61d8d239fd16f.js +0 -1
  301. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-bd0aff5a5ed8888c.js +0 -1
  302. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-d1ee961387c58b7f.js +0 -1
  303. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers/[...slug]-f028ef3880ed856c.js +0 -1
  304. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-ceb06e1616ee9610.js +0 -1
  305. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-8ff16ef9566e911a.js +0 -1
  306. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-74d76300942dcee8.js +0 -1
  307. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-d7a8bc51bb7a1082.js +0 -1
  308. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/sync-data-79a4cf66a623e667.js +0 -1
  309. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/sign-in-f59d34429fe022ee.js +0 -1
  310. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-5753fac7c1bfdc88.js +0 -1
  311. /mage_ai/server/frontend_dist/_next/static/{i8pymuJDTVHdWjUP1QSh1 → RhDiJSkcjCsh4xxX4BFBk}/_ssgManifest.js +0 -0
  312. /mage_ai/server/frontend_dist_base_path_template/_next/static/{CKCvjsYCf2imD2X8zAOBf → TdpLLFome13qvM0gXvpHs}/_ssgManifest.js +0 -0
  313. {mage_ai-0.9.68.dist-info → mage_ai-0.9.70.dist-info}/LICENSE +0 -0
  314. {mage_ai-0.9.68.dist-info → mage_ai-0.9.70.dist-info}/WHEEL +0 -0
  315. {mage_ai-0.9.68.dist-info → mage_ai-0.9.70.dist-info}/entry_points.txt +0 -0
  316. {mage_ai-0.9.68.dist-info → mage_ai-0.9.70.dist-info}/top_level.txt +0 -0
mage_ai/io/bigquery.py CHANGED
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from typing import Dict, List, Mapping, Union
2
3
 
3
4
  import numpy as np
@@ -14,6 +15,7 @@ from sqlglot import exp, parse_one
14
15
 
15
16
  from mage_ai.io.base import QUERY_ROW_LIMIT, BaseSQLDatabase, ExportWritePolicy
16
17
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
18
+ from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
17
19
  from mage_ai.io.export_utils import infer_dtypes
18
20
  from mage_ai.shared.custom_logger import DX_PRINTER
19
21
  from mage_ai.shared.environments import is_debug
@@ -200,6 +202,9 @@ WHERE TABLE_NAME = '{table_name}'
200
202
  overwrite_types: Dict = None,
201
203
  query_string: Union[str, None] = None,
202
204
  verbose: bool = True,
205
+ unique_conflict_method: str = None,
206
+ unique_constraints: List[str] = None,
207
+ write_disposition: str = None,
203
208
  **configuration_params,
204
209
  ) -> None:
205
210
  """
@@ -230,24 +235,23 @@ WHERE TABLE_NAME = '{table_name}'
230
235
  elif type(df) is list:
231
236
  df = DataFrame(df)
232
237
 
233
- def __process(database: Union[str, None]):
234
- if query_string:
235
- parts = table_id.split('.')
236
- if len(parts) == 2:
237
- schema, table_name = parts
238
- elif len(parts) == 3:
239
- database, schema, table_name = parts
238
+ def __process(database: Union[str, None], write_disposition: str = None):
239
+ parts = table_id.split('.')
240
+ if len(parts) == 2:
241
+ schema, table_name = parts
242
+ elif len(parts) == 3:
243
+ database, schema, table_name = parts
240
244
 
241
- df_existing = self.client.query(f"""
245
+ df_existing = self.client.query(f"""
242
246
  SELECT 1
243
247
  FROM `{database}.{schema}.__TABLES_SUMMARY__`
244
248
  WHERE table_id = '{table_name}'
245
249
  """).to_dataframe()
246
250
 
247
- full_table_name = f'`{database}.{schema}.{table_name}`'
248
-
249
- table_doesnt_exist = df_existing.empty
251
+ full_table_name = f'`{database}.{schema}.{table_name}`'
250
252
 
253
+ table_doesnt_exist = df_existing.empty
254
+ if query_string:
251
255
  if ExportWritePolicy.FAIL == if_exists and not table_doesnt_exist:
252
256
  raise ValueError(
253
257
  f'Table \'{full_table_name}\' already exists in database.',
@@ -268,60 +272,129 @@ WHERE table_id = '{table_name}'
268
272
  self.client.query(sql)
269
273
 
270
274
  else:
271
- config = LoadJobConfig(**configuration_params)
272
- if overwrite_types is not None:
273
- config.schema = [SchemaField(k, v) for k, v in overwrite_types.items()]
274
- if 'write_disposition' not in configuration_params:
275
- if if_exists == 'replace':
276
- config.write_disposition = WriteDisposition.WRITE_TRUNCATE
277
- elif if_exists == 'append':
278
- config.write_disposition = WriteDisposition.WRITE_APPEND
279
- elif if_exists == 'fail':
280
- config.write_disposition = WriteDisposition.WRITE_EMPTY
281
- else:
282
- raise ValueError(
283
- f'Invalid policy specified for handling existence of '
284
- f'table: \'{if_exists}\''
275
+ if (
276
+ if_exists == ExportWritePolicy.APPEND
277
+ and not table_doesnt_exist
278
+ and unique_constraints
279
+ and unique_conflict_method
280
+ ):
281
+ temp_table_id = f'{table_id}_{uuid.uuid4().hex}'
282
+
283
+ try:
284
+ self.__write_table(
285
+ df,
286
+ temp_table_id,
287
+ overwrite_types=overwrite_types,
288
+ **configuration_params,
289
+ )
290
+
291
+ parts = temp_table_id.split('.')
292
+ if len(parts) == 2:
293
+ temp_table_name = parts[1]
294
+ elif len(parts) == 3:
295
+ temp_table_name = parts[2]
296
+ column_types = self.get_column_types(schema, temp_table_name)
297
+ columns = list(column_types.keys())
298
+ if not columns:
299
+ columns = df.columns.str.replace(' ', '_')
300
+
301
+ on_conditions = []
302
+ for col in unique_constraints:
303
+ on_conditions.append(
304
+ f'((a.{col} IS NULL AND b.{col} IS NULL) OR a.{col} = b.{col})',
305
+ )
306
+
307
+ insert_columns = ', '.join([f'`{col}`' for col in columns])
308
+
309
+ merge_commands = [
310
+ f'MERGE INTO `{table_id}` AS a',
311
+ f'USING (SELECT * FROM `{temp_table_id}`) AS b',
312
+ f"ON {' AND '.join(on_conditions)}",
313
+ ]
314
+
315
+ if UNIQUE_CONFLICT_METHOD_UPDATE == unique_conflict_method:
316
+ set_command = ', '.join(
317
+ [f'a.`{col}` = b.`{col}`' for col in columns],
318
+ )
319
+ merge_commands.append(f'WHEN MATCHED THEN UPDATE SET {set_command}')
320
+
321
+ merge_values = f"({', '.join([f'b.`{col}`' for col in columns])})"
322
+ merge_commands.append(
323
+ f'WHEN NOT MATCHED THEN INSERT ({insert_columns}) VALUES {merge_values}', # noqa: E501
285
324
  )
286
- parts = table_id.split('.')
287
- if len(parts) == 2:
288
- schema = parts[0]
289
- table_name = parts[1]
290
- elif len(parts) == 3:
291
- schema = parts[1]
292
- table_name = parts[2]
293
-
294
- self.client.create_dataset(dataset=schema, exists_ok=True)
295
-
296
- column_types = self.get_column_types(schema, table_name)
297
-
298
- if df is not None:
299
- df.fillna(value=np.NaN, inplace=True)
300
- for col in df.columns:
301
- col_type = column_types.get(col)
302
- if not col_type:
303
- continue
304
-
305
- null_rows = df[col].isnull()
306
- if col_type.startswith('ARRAY<STRUCT'):
307
- df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [{}])
308
- elif col_type.startswith('ARRAY'):
309
- df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [])
310
- elif col_type.startswith('STRUCT'):
311
- df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: {})
312
-
313
- # Clean column names
314
- if type(df) is DataFrame:
315
- df.columns = df.columns.str.replace(' ', '_')
316
-
317
- self.client.load_table_from_dataframe(df, table_id, job_config=config).result()
325
+
326
+ merge_command = '\n'.join(merge_commands)
327
+
328
+ self.client.query(merge_command).result()
329
+ finally:
330
+ self.client.query(f'DROP TABLE IF EXISTS {temp_table_id}').result()
331
+ else:
332
+ if not write_disposition:
333
+ if if_exists == ExportWritePolicy.APPEND:
334
+ write_disposition = WriteDisposition.WRITE_APPEND
335
+ elif if_exists == ExportWritePolicy.REPLACE:
336
+ write_disposition = WriteDisposition.WRITE_TRUNCATE
337
+ elif if_exists == ExportWritePolicy.FAIL:
338
+ write_disposition = WriteDisposition.WRITE_EMPTY
339
+ self.__write_table(
340
+ df,
341
+ table_id,
342
+ overwrite_types=overwrite_types,
343
+ write_disposition=write_disposition,
344
+ **configuration_params,
345
+ )
318
346
 
319
347
  if verbose:
320
348
  with self.printer.print_msg(f'Exporting data to table \'{table_id}\''):
321
- __process(database=database)
349
+ __process(database=database, write_disposition=write_disposition)
322
350
  else:
323
351
  __process(database=database)
324
352
 
353
+ def __write_table(
354
+ self,
355
+ df: DataFrame,
356
+ table_id: str,
357
+ overwrite_types: Dict = None,
358
+ **configuration_params,
359
+ ):
360
+ config = LoadJobConfig(**configuration_params)
361
+ if overwrite_types is not None:
362
+ config.schema = [SchemaField(k, v) for k, v in overwrite_types.items()]
363
+ if not config.write_disposition:
364
+ config.write_disposition = WriteDisposition.WRITE_APPEND
365
+ parts = table_id.split('.')
366
+ if len(parts) == 2:
367
+ schema = parts[0]
368
+ table_name = parts[1]
369
+ elif len(parts) == 3:
370
+ schema = parts[1]
371
+ table_name = parts[2]
372
+
373
+ self.client.create_dataset(dataset=schema, exists_ok=True)
374
+
375
+ column_types = self.get_column_types(schema, table_name)
376
+
377
+ if df is not None:
378
+ df.fillna(value=np.NaN, inplace=True)
379
+ for col in df.columns:
380
+ col_type = column_types.get(col)
381
+ if not col_type:
382
+ continue
383
+
384
+ null_rows = df[col].isnull()
385
+ if col_type.startswith('ARRAY<STRUCT'):
386
+ df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [{}])
387
+ elif col_type.startswith('ARRAY'):
388
+ df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: [])
389
+ elif col_type.startswith('STRUCT'):
390
+ df.loc[null_rows, col] = df.loc[null_rows, col].apply(lambda x: {})
391
+
392
+ # Clean column names
393
+ if type(df) is DataFrame:
394
+ df.columns = df.columns.str.replace(' ', '_')
395
+
396
+ return self.client.load_table_from_dataframe(df, table_id, job_config=config).result()
397
+
325
398
  def execute(self, query_string: str, **kwargs) -> None:
326
399
  """
327
400
  Sends query to the connected BigQuery warehouse.
mage_ai/io/mysql.py CHANGED
@@ -8,6 +8,7 @@ from mysql.connector.cursor import MySQLCursor
8
8
  from pandas import DataFrame, Series
9
9
 
10
10
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
11
+ from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
11
12
  from mage_ai.io.export_utils import PandasTypes
12
13
  from mage_ai.io.sql import BaseSQL
13
14
  from mage_ai.shared.parsers import encode_complex
@@ -60,7 +61,7 @@ class MySQL(BaseSQL):
60
61
  ) -> str:
61
62
  if unique_constraints is None:
62
63
  unique_constraints = []
63
- query = []
64
+ columns_and_types = []
64
65
  for cname in dtypes:
65
66
  if overwrite_types is not None and cname in overwrite_types.keys():
66
67
  dtypes[cname] = overwrite_types[cname]
@@ -68,9 +69,22 @@ class MySQL(BaseSQL):
68
69
  cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
69
70
  else:
70
71
  cleaned_col_name = cname
71
- query.append(f'`{cleaned_col_name}` {dtypes[cname]} NULL')
72
-
73
- return f'CREATE TABLE {table_name} (' + ','.join(query) + ');'
72
+ columns_and_types.append(f'`{cleaned_col_name}` {dtypes[cname]} NULL')
73
+
74
+ if unique_constraints:
75
+ unique_constraints = [
76
+ clean_name(col, case_sensitive=case_sensitive)
77
+ for col in unique_constraints
78
+ ]
79
+ index_name = '_'.join([
80
+ clean_name(table_name, case_sensitive=case_sensitive),
81
+ ] + unique_constraints)
82
+ index_name = f'unique{index_name}'[:64]
83
+ columns_and_types.append(
84
+ f"CONSTRAINT {index_name} Unique({', '.join(unique_constraints)})"
85
+ )
86
+
87
+ return f'CREATE TABLE {table_name} (' + ','.join(columns_and_types) + ');'
74
88
 
75
89
  def open(self) -> None:
76
90
  with self.printer.print_msg('Opening connection to MySQL database'):
@@ -94,6 +108,9 @@ class MySQL(BaseSQL):
94
108
  dtypes: List[str],
95
109
  full_table_name: str,
96
110
  buffer: Union[IO, None] = None,
111
+ case_sensitive: bool = False,
112
+ unique_constraints: List[str] = None,
113
+ unique_conflict_method: str = None,
97
114
  **kwargs,
98
115
  ) -> None:
99
116
  def serialize_obj(val):
@@ -133,9 +150,24 @@ class MySQL(BaseSQL):
133
150
  for _, row in df_.iterrows():
134
151
  values.append(tuple([str(val) if type(val) is pd.Timestamp else val for val in row]))
135
152
 
136
- insert_columns = ', '.join([f'`{col}`'for col in columns])
153
+ cleaned_columns = [clean_name(col, case_sensitive=case_sensitive) for col in columns]
154
+ insert_columns = ', '.join([f'`{col}`'for col in cleaned_columns])
155
+
156
+ query = [
157
+ f'INSERT INTO {full_table_name} ({insert_columns})',
158
+ f'VALUES ({values_placeholder})',
159
+ ]
160
+
161
+ if unique_constraints and unique_conflict_method:
162
+ if UNIQUE_CONFLICT_METHOD_UPDATE == unique_conflict_method:
163
+ update_command = [f'{col} = new.{col}' for col in cleaned_columns]
164
+ query += [
165
+ 'AS new',
166
+ f"ON DUPLICATE KEY UPDATE {', '.join(update_command)}",
167
+ ]
168
+
169
+ sql = '\n'.join(query)
137
170
 
138
- sql = f'INSERT INTO {full_table_name} ({insert_columns}) VALUES ({values_placeholder})'
139
171
  cursor.executemany(sql, values)
140
172
 
141
173
  def get_type(self, column: Series, dtype: str) -> str:
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,
@@ -0,0 +1,54 @@
1
+ """Update token column type
2
+
3
+ Revision ID: 42a14d6143f1
4
+ Revises: b9a2d6d0a2c7
5
+ Create Date: 2024-04-12 15:19:52.639580
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '42a14d6143f1'
14
+ down_revision = 'b9a2d6d0a2c7'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ with op.batch_alter_table('oauth2_access_token', schema=None) as batch_op:
22
+ batch_op.alter_column(
23
+ 'token',
24
+ existing_type=sa.String(length=255),
25
+ type_=sa.Text(),
26
+ existing_nullable=True,
27
+ )
28
+ batch_op.alter_column(
29
+ 'refresh_token',
30
+ existing_type=sa.String(length=255),
31
+ type_=sa.Text(),
32
+ existing_nullable=True,
33
+ )
34
+
35
+ # ### end Alembic commands ###
36
+
37
+
38
+ def downgrade() -> None:
39
+ # ### commands auto generated by Alembic - please adjust! ###
40
+ with op.batch_alter_table('oauth2_access_token', schema=None) as batch_op:
41
+ batch_op.alter_column(
42
+ 'token',
43
+ existing_type=sa.Text(),
44
+ type_=sa.String(length=255),
45
+ existing_nullable=True,
46
+ )
47
+ batch_op.alter_column(
48
+ 'refresh_token',
49
+ existing_type=sa.String(length=255),
50
+ type_=sa.Text(),
51
+ existing_nullable=True,
52
+ )
53
+
54
+ # ### end Alembic commands ###
@@ -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,
@@ -12,6 +12,7 @@ from sqlalchemy import (
12
12
  ForeignKey,
13
13
  Integer,
14
14
  String,
15
+ Text,
15
16
  and_,
16
17
  asc,
17
18
  func,
@@ -324,10 +325,10 @@ class Role(BaseModel):
324
325
  @classmethod
325
326
  @safe_db_query
326
327
  def create_default_roles(
327
- self,
328
+ cls,
328
329
  entity: Entity = None,
329
330
  entity_id: str = None,
330
- prefix: str = None,
331
+ name_func: Callable[[str], str] = None,
331
332
  ) -> None:
332
333
  """
333
334
  Create default roles with associated permissions for a given entity and entity_id.
@@ -342,18 +343,18 @@ class Role(BaseModel):
342
343
  entity = Entity.GLOBAL
343
344
  permissions = Permission.create_default_permissions(entity=entity, entity_id=entity_id)
344
345
  mapping = {
345
- self.DefaultRole.OWNER: Permission.Access.OWNER,
346
- self.DefaultRole.ADMIN: Permission.Access.ADMIN,
347
- self.DefaultRole.EDITOR: Permission.Access.EDITOR,
348
- self.DefaultRole.VIEWER: Permission.Access.VIEWER,
346
+ cls.DefaultRole.OWNER: Permission.Access.OWNER,
347
+ cls.DefaultRole.ADMIN: Permission.Access.ADMIN,
348
+ cls.DefaultRole.EDITOR: Permission.Access.EDITOR,
349
+ cls.DefaultRole.VIEWER: Permission.Access.VIEWER,
349
350
  }
350
351
  for name, access in mapping.items():
351
352
  role_name = name
352
- if prefix is not None:
353
- role_name = f'{prefix}_{name}'
354
- role = self.query.filter(self.name == role_name).first()
353
+ if name_func is not None:
354
+ role_name = name_func(name)
355
+ role = cls.query.filter(Role.name == role_name).first()
355
356
  if not role:
356
- self.create(
357
+ cls.create(
357
358
  name=role_name,
358
359
  permissions=[
359
360
  Permission.query.filter(
@@ -880,10 +881,10 @@ class Oauth2AccessToken(BaseModel):
880
881
  expires = Column(DateTime(timezone=True))
881
882
  oauth2_application = relationship(Oauth2Application, back_populates='oauth2_access_tokens')
882
883
  oauth2_application_id = Column(Integer, ForeignKey('oauth2_application.id'))
883
- token = Column(String(255), index=True, unique=True)
884
+ token = Column(Text, index=True, unique=True)
884
885
  user = relationship(User, back_populates='oauth2_access_tokens')
885
886
  user_id = Column(Integer, ForeignKey('user.id'))
886
- refresh_token = Column(String(255))
887
+ refresh_token = Column(Text)
887
888
 
888
889
  def is_valid(self) -> bool:
889
890
  return self.token and \