mage-ai 0.9.75__py3-none-any.whl → 0.9.77__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 (226) hide show
  1. mage_ai/api/policies/PipelineSchedulePolicy.py +1 -0
  2. mage_ai/api/presenters/PipelineSchedulePresenter.py +11 -2
  3. mage_ai/api/resources/GitFileResource.py +8 -0
  4. mage_ai/api/resources/PipelineScheduleResource.py +11 -3
  5. mage_ai/api/resources/PipelineTriggerResource.py +3 -1
  6. mage_ai/api/resources/SessionResource.py +2 -2
  7. mage_ai/api/resources/SyncResource.py +1 -1
  8. mage_ai/api/resources/UserResource.py +1 -1
  9. mage_ai/cli/main.py +6 -1
  10. mage_ai/data_integrations/destinations/constants.py +2 -0
  11. mage_ai/data_integrations/sources/constants.py +2 -0
  12. mage_ai/data_preparation/executors/block_executor.py +8 -3
  13. mage_ai/data_preparation/executors/pipeline_executor.py +35 -19
  14. mage_ai/data_preparation/models/block/__init__.py +29 -23
  15. mage_ai/data_preparation/models/block/dbt/dbt_adapter.py +20 -8
  16. mage_ai/data_preparation/models/block/dynamic/constants.py +0 -1
  17. mage_ai/data_preparation/models/block/dynamic/counter.py +1 -3
  18. mage_ai/data_preparation/models/block/outputs.py +1 -1
  19. mage_ai/data_preparation/models/block/r/__init__.py +16 -5
  20. mage_ai/data_preparation/models/block/sql/__init__.py +2 -0
  21. mage_ai/data_preparation/models/block/sql/mssql.py +8 -0
  22. mage_ai/data_preparation/models/block/sql/utils/shared.py +6 -2
  23. mage_ai/data_preparation/models/constants.py +3 -0
  24. mage_ai/data_preparation/models/pipeline.py +1 -1
  25. mage_ai/data_preparation/storage/local_storage.py +4 -1
  26. mage_ai/data_preparation/templates/constants.py +7 -0
  27. mage_ai/data_preparation/templates/data_loaders/airtable.py +28 -0
  28. mage_ai/data_preparation/templates/repo/io_config.yaml +2 -0
  29. mage_ai/io/airtable.py +104 -0
  30. mage_ai/io/base.py +1 -0
  31. mage_ai/io/bigquery.py +36 -0
  32. mage_ai/io/config.py +6 -0
  33. mage_ai/io/mssql.py +21 -9
  34. mage_ai/io/mysql.py +6 -1
  35. mage_ai/io/postgres.py +3 -0
  36. mage_ai/io/redshift.py +13 -0
  37. mage_ai/io/sql.py +1 -0
  38. mage_ai/orchestration/db/__init__.py +20 -0
  39. mage_ai/orchestration/db/migrations/versions/39d36f1dab73_create_genericjob.py +47 -0
  40. mage_ai/orchestration/db/models/oauth.py +2 -1
  41. mage_ai/orchestration/db/models/schedules.py +107 -5
  42. mage_ai/orchestration/db/models/secrets.py +11 -1
  43. mage_ai/orchestration/job_manager.py +19 -0
  44. mage_ai/orchestration/metrics/pipeline_run.py +1 -1
  45. mage_ai/orchestration/notification/sender.py +2 -2
  46. mage_ai/orchestration/pipeline_scheduler_original.py +150 -6
  47. mage_ai/orchestration/pipeline_scheduler_project_platform.py +4 -5
  48. mage_ai/orchestration/queue/config.py +11 -1
  49. mage_ai/orchestration/queue/process_queue.py +2 -0
  50. mage_ai/orchestration/utils/distributed_lock.py +8 -1
  51. mage_ai/server/api/base.py +41 -0
  52. mage_ai/server/api/constants.py +1 -0
  53. mage_ai/server/api/triggers.py +9 -0
  54. mage_ai/server/constants.py +1 -1
  55. mage_ai/server/frontend_dist/404.html +2 -2
  56. mage_ai/server/frontend_dist/_next/static/chunks/449-5e2253c6aba42557.js +1 -0
  57. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-782dd4a6b13e1c42.js +2 -0
  58. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-5db54821a3059c69.js +1 -0
  59. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-f65416f6dbe30ad3.js +1 -0
  60. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-03d9bca3bc5e6088.js +1 -0
  61. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-d25d07db166cbb04.js +1 -0
  62. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-fa61dc6c1370e6a5.js +1 -0
  63. mage_ai/server/frontend_dist/_next/static/chunks/{webpack-0bc44da590c7cf85.js → webpack-b9a067f3bd0a3a05.js} +1 -1
  64. mage_ai/server/frontend_dist/_next/static/{38-PtskJFUTYUpRhT1qF_ → qR0jauUABqPaFMjUsYeoG}/_buildManifest.js +1 -1
  65. mage_ai/server/frontend_dist/block-layout.html +2 -2
  66. mage_ai/server/frontend_dist/compute.html +2 -2
  67. mage_ai/server/frontend_dist/files.html +2 -2
  68. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  69. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  70. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  71. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  72. mage_ai/server/frontend_dist/index.html +2 -2
  73. mage_ai/server/frontend_dist/manage/files.html +2 -2
  74. mage_ai/server/frontend_dist/manage/overview.html +2 -2
  75. mage_ai/server/frontend_dist/manage/pipeline-runs.html +2 -2
  76. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  77. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  78. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  79. mage_ai/server/frontend_dist/manage/users.html +2 -2
  80. mage_ai/server/frontend_dist/manage.html +2 -2
  81. mage_ai/server/frontend_dist/oauth.html +3 -3
  82. mage_ai/server/frontend_dist/overview.html +2 -2
  83. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  84. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  85. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  86. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  87. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  88. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  89. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  90. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  91. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  92. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  93. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  94. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  95. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  96. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  97. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  98. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  99. mage_ai/server/frontend_dist/pipelines.html +2 -2
  100. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
  101. mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
  102. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  103. mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
  104. mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
  105. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  106. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  107. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  108. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  109. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  110. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  111. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  112. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  113. mage_ai/server/frontend_dist/settings.html +2 -2
  114. mage_ai/server/frontend_dist/sign-in.html +5 -5
  115. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  116. mage_ai/server/frontend_dist/templates.html +2 -2
  117. mage_ai/server/frontend_dist/terminal.html +2 -2
  118. mage_ai/server/frontend_dist/test.html +2 -2
  119. mage_ai/server/frontend_dist/triggers.html +2 -2
  120. mage_ai/server/frontend_dist/v2/canvas.html +2 -2
  121. mage_ai/server/frontend_dist/v2.html +2 -2
  122. mage_ai/server/frontend_dist/version-control.html +2 -2
  123. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  124. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/449-5e2253c6aba42557.js +1 -0
  125. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-ee5e328aaf51c698.js +2 -0
  126. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users-5db54821a3059c69.js +1 -0
  127. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-f65416f6dbe30ad3.js +1 -0
  128. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-03d9bca3bc5e6088.js +1 -0
  129. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-d25d07db166cbb04.js +1 -0
  130. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users-fa61dc6c1370e6a5.js +1 -0
  131. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{webpack-12ad70eb5c31aa92.js → webpack-5f4be622608d9267.js} +1 -1
  132. mage_ai/server/frontend_dist_base_path_template/_next/static/{dxnSzgIvSG4Ke5LM-tPQX → iCySon3_GCldnbC5U7C-s}/_buildManifest.js +1 -1
  133. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  134. mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
  135. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  136. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  137. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  138. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
  139. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
  140. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  141. mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
  142. mage_ai/server/frontend_dist_base_path_template/manage/overview.html +2 -2
  143. mage_ai/server/frontend_dist_base_path_template/manage/pipeline-runs.html +2 -2
  144. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  145. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  146. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  147. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  148. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  149. mage_ai/server/frontend_dist_base_path_template/oauth.html +3 -3
  150. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  151. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  152. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  153. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  154. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
  155. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  156. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  157. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  158. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  159. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  160. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  161. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  162. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  163. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  164. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  165. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  166. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  167. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  168. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
  169. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
  170. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  171. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
  172. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
  173. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
  174. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
  175. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  176. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
  177. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
  178. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  179. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
  180. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  181. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  182. mage_ai/server/frontend_dist_base_path_template/sign-in.html +5 -5
  183. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  184. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  185. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  186. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  187. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  188. mage_ai/server/frontend_dist_base_path_template/v2/canvas.html +2 -2
  189. mage_ai/server/frontend_dist_base_path_template/v2.html +2 -2
  190. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  191. mage_ai/server/scheduler_manager.py +2 -0
  192. mage_ai/server/terminal_server.py +3 -0
  193. mage_ai/settings/server.py +4 -0
  194. mage_ai/streaming/sources/kafka.py +2 -1
  195. mage_ai/tests/data_preparation/executors/test_block_executor.py +3 -3
  196. mage_ai/tests/data_preparation/models/block/dynamic/test_counter.py +1 -3
  197. mage_ai/tests/data_preparation/models/test_variable.py +2 -0
  198. mage_ai/tests/io/create_table/test_postgresql.py +3 -2
  199. mage_ai/tests/orchestration/notification/test_sender.py +5 -1
  200. mage_ai/tests/streaming/sources/test_kafka.py +2 -2
  201. mage_ai/usage_statistics/logger.py +99 -15
  202. mage_ai-0.9.77.dist-info/METADATA +356 -0
  203. {mage_ai-0.9.75.dist-info → mage_ai-0.9.77.dist-info}/RECORD +211 -208
  204. {mage_ai-0.9.75.dist-info → mage_ai-0.9.77.dist-info}/WHEEL +1 -1
  205. mage_ai/server/frontend_dist/_next/static/chunks/449-f689774546860ca4.js +0 -1
  206. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-13bf3b7dcef50c29.js +0 -2
  207. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-b99379d0aa6a8c25.js +0 -1
  208. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-e51cd04bd4d1fffe.js +0 -1
  209. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-0abf8a1b7243f93b.js +0 -1
  210. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-38187954b6ec4b40.js +0 -1
  211. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-3ee783f5139c76a1.js +0 -1
  212. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/449-f689774546860ca4.js +0 -1
  213. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-0392ef723ea2c6f8.js +0 -2
  214. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users-b99379d0aa6a8c25.js +0 -1
  215. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-e51cd04bd4d1fffe.js +0 -1
  216. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-0abf8a1b7243f93b.js +0 -1
  217. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-38187954b6ec4b40.js +0 -1
  218. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users-3ee783f5139c76a1.js +0 -1
  219. mage_ai-0.9.75.dist-info/METADATA +0 -377
  220. /mage_ai/server/frontend_dist/_next/static/chunks/pages/{_app-13bf3b7dcef50c29.js.LICENSE.txt → _app-782dd4a6b13e1c42.js.LICENSE.txt} +0 -0
  221. /mage_ai/server/frontend_dist/_next/static/{38-PtskJFUTYUpRhT1qF_ → qR0jauUABqPaFMjUsYeoG}/_ssgManifest.js +0 -0
  222. /mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{_app-0392ef723ea2c6f8.js.LICENSE.txt → _app-ee5e328aaf51c698.js.LICENSE.txt} +0 -0
  223. /mage_ai/server/frontend_dist_base_path_template/_next/static/{dxnSzgIvSG4Ke5LM-tPQX → iCySon3_GCldnbC5U7C-s}/_ssgManifest.js +0 -0
  224. {mage_ai-0.9.75.dist-info → mage_ai-0.9.77.dist-info}/entry_points.txt +0 -0
  225. {mage_ai-0.9.75.dist-info → mage_ai-0.9.77.dist-info/licenses}/LICENSE +0 -0
  226. {mage_ai-0.9.75.dist-info → mage_ai-0.9.77.dist-info}/top_level.txt +0 -0
@@ -19,6 +19,13 @@ GROUP_ROW_ACTIONS = 'Row actions'
19
19
  GROUP_SHIFT = 'Shift'
20
20
 
21
21
  TEMPLATES = [
22
+ dict(
23
+ block_type=BlockType.DATA_LOADER,
24
+ description='Load a Table from Airtable App.',
25
+ language=BlockLanguage.PYTHON,
26
+ name='Airtable',
27
+ path='data_loaders/airtable.py',
28
+ ),
22
29
  dict(
23
30
  block_type=BlockType.DATA_LOADER,
24
31
  description='Load a Delta Table from Amazon S3.',
@@ -0,0 +1,28 @@
1
+ {% extends "data_loaders/default.jinja" %}
2
+ {% block imports %}
3
+ from mage_ai.settings.repo import get_repo_path
4
+ from mage_ai.io.config import ConfigFileLoader
5
+ from mage_ai.io.airtable import Airtable
6
+ from os import path
7
+ {{ super() -}}
8
+ {% endblock %}
9
+
10
+
11
+ {% block content %}
12
+ @data_loader
13
+ def load_data_from_airtable(*args, **kwargs):
14
+ """
15
+ Template for loading data from Airtable.
16
+ Specify your configuration settings in 'io_config.yaml'.
17
+ """
18
+ config_path = path.join(get_repo_path(), 'io_config.yaml')
19
+ config_profile = 'default'
20
+
21
+ base_id = 'your_base_id'
22
+ table_name = 'your_table_name'
23
+
24
+ return Airtable.with_config(ConfigFileLoader(config_path, config_profile)).load(
25
+ base_id=base_id,
26
+ table_name=table_name
27
+ )
28
+ {% endblock %}
@@ -11,6 +11,8 @@ default:
11
11
  ALGOLIA_APP_ID: app_id
12
12
  ALGOLIA_API_KEY: api_key
13
13
  ALGOLIA_INDEX_NAME: index_name
14
+ # Airtable
15
+ AIRTABLE_ACCESS_TOKEN: token
14
16
  # Azure
15
17
  AZURE_CLIENT_ID: "{{ env_var('AZURE_CLIENT_ID') }}"
16
18
  AZURE_CLIENT_SECRET: "{{ env_var('AZURE_CLIENT_SECRET') }}"
mage_ai/io/airtable.py ADDED
@@ -0,0 +1,104 @@
1
+ from typing import Union
2
+
3
+ import pandas as pd
4
+ import polars as pl
5
+ from pyairtable.api import Api
6
+
7
+ from mage_ai.io.base import BaseIO
8
+ from mage_ai.io.config import BaseConfigLoader, ConfigKey
9
+
10
+
11
+ class Airtable(BaseIO):
12
+ """
13
+ Handles data transfer between Airtable tables and the Mage app.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ token: str,
19
+ verbose: bool = True,
20
+ **kwargs) -> None:
21
+ """
22
+ Initializes a connection to Airtable.
23
+
24
+ Args:
25
+ token (str): Airtable API token.
26
+ verbose (bool, optional): Whether to print verbose output. Defaults to True.
27
+ **kwargs: Additional keyword arguments to pass to the pyairtable Api constructor.
28
+ """
29
+ super().__init__(verbose=verbose)
30
+ self.client = Api(token, **kwargs) # Create the Airtable API client
31
+
32
+ @classmethod
33
+ def with_config(
34
+ cls,
35
+ config: BaseConfigLoader
36
+ ) -> 'Airtable':
37
+ """
38
+ Initializes an Airtable client from a configuration loader.
39
+
40
+ Args:
41
+ config (BaseConfigLoader): Configuration loader object.
42
+
43
+ Raises:
44
+ ValueError: If no valid Airtable API token is found in the configuration.
45
+
46
+ Returns:
47
+ Airtable: An instance of the Airtable class.
48
+ """
49
+ if ConfigKey.AIRTABLE_ACCESS_TOKEN not in config:
50
+ raise ValueError(
51
+ 'No valid API token found for Airtable.'
52
+ 'You must specify your access token in your config.'
53
+ )
54
+
55
+ return cls(
56
+ token=config[ConfigKey.AIRTABLE_ACCESS_TOKEN]
57
+ )
58
+
59
+ def load(
60
+ self,
61
+ base_id: str,
62
+ table_name: str,
63
+ **kwargs,
64
+ ) -> pd.DataFrame:
65
+ """
66
+ Loads data from an Airtable table into a Pandas DataFrame.
67
+
68
+ Args:
69
+ base_id (str): The ID of the Airtable base (e.g., 'app*****').
70
+ table_name (str): The name or ID of the Airtable table (e.g., 'tbl*****').
71
+ **kwargs: Additional keyword arguments to pass to the pyairtable table.all() method.
72
+
73
+ Returns:
74
+ DataFrame: A Pandas DataFrame containing the data from the Airtable table.
75
+ """
76
+ with self.printer.print_msg(
77
+ f'Loading data frame from table \'{table_name}\' at airtable app \'{base_id}\''
78
+ ):
79
+ table = self.client.table(base_id, table_name) # Get the Airtable table
80
+ data = table.all(**kwargs) # Fetch all records from the table
81
+
82
+ # Flatten the Airtable data structure into a list of dictionaries
83
+ flattened_data = []
84
+ for record in data:
85
+ flattened_record = {
86
+ 'id': record['id'],
87
+ 'createdTime': record['createdTime']
88
+ }
89
+ fields = record['fields']
90
+ flattened_record.update(fields)
91
+ flattened_data.append(flattened_record)
92
+
93
+ return pd.DataFrame(flattened_data) # Create and return a DataFrame
94
+
95
+ def export(
96
+ self,
97
+ df: Union[pd.DataFrame, pl.DataFrame],
98
+ *args,
99
+ **kwargs
100
+ ) -> None:
101
+ """
102
+ Not implemented yet. This method is intended to export data to Airtable.
103
+ """
104
+ pass
mage_ai/io/base.py CHANGED
@@ -16,6 +16,7 @@ QUERY_ROW_LIMIT = 10_000_000
16
16
 
17
17
 
18
18
  class DataSource(StrEnum):
19
+ AIRTABLE = 'airtable'
19
20
  ALGOLIA = 'algolia'
20
21
  API = 'api'
21
22
  BIGQUERY = 'bigquery'
mage_ai/io/bigquery.py CHANGED
@@ -1,7 +1,9 @@
1
+ import traceback
1
2
  import uuid
2
3
  from typing import Dict, List, Mapping, Union
3
4
 
4
5
  import numpy as np
6
+ import pandas as pd
5
7
  from google.cloud.bigquery import (
6
8
  Client,
7
9
  LoadJobConfig,
@@ -399,7 +401,41 @@ WHERE table_id = '{table_name}'
399
401
  # Clean column names
400
402
  if type(df) is DataFrame:
401
403
  df.columns = df.columns.str.replace(' ', '_')
404
+ table = None
405
+ try:
406
+ # Cast column types
407
+ table = self.client.get_table(table_id)
408
+ except Exception:
409
+ print(f'Table {table_id} does not exist.')
410
+ pass
402
411
 
412
+ if table is not None:
413
+ try:
414
+ timestamp_columns = [field.name for field in table.schema
415
+ if field.field_type == 'TIMESTAMP']
416
+
417
+ # Convert TIMESTAMP columns in DataFrame
418
+ for col in timestamp_columns:
419
+ if col in df.columns:
420
+ df[col] = pd.to_datetime(df[col])
421
+ except Exception:
422
+ print('Fail to cast column types in dataframe.')
423
+ traceback.print_exc()
424
+ if (
425
+ not config.schema and
426
+ config.write_disposition == WriteDisposition.WRITE_TRUNCATE
427
+ ):
428
+ df_columns = df.columns.tolist()
429
+ config.schema = [
430
+ SchemaField(
431
+ field.name,
432
+ field.field_type,
433
+ mode=field.mode,
434
+ fields=field.fields,
435
+ )
436
+ for field in table.schema
437
+ if field.name in df_columns
438
+ ]
403
439
  return self.client.load_table_from_dataframe(df, table_id, job_config=config).result()
404
440
 
405
441
  def execute(self, query_string: str, **kwargs) -> None:
mage_ai/io/config.py CHANGED
@@ -16,6 +16,8 @@ class ConfigKey(StrEnum):
16
16
  List of configuration settings for use with data IO clients.
17
17
  """
18
18
 
19
+ AIRTABLE_ACCESS_TOKEN = "AIRTABLE_ACCESS_TOKEN"
20
+
19
21
  ALGOLIA_APP_ID = 'ALGOLIA_APP_ID'
20
22
  ALGOLIA_API_KEY = 'ALGOLIA_API_KEY'
21
23
  ALGOLIA_INDEX_NAME = 'ALGOLIA_INDEX_NAME'
@@ -64,6 +66,7 @@ class ConfigKey(StrEnum):
64
66
  MONGODB_PORT = 'MONGODB_PORT'
65
67
  MONGODB_USER = 'MONGODB_USER'
66
68
 
69
+ MSSQL_AUTHENTICATION = 'MSSQL_AUTHENTICATION'
67
70
  MSSQL_DATABASE = 'MSSQL_DATABASE'
68
71
  MSSQL_DRIVER = 'MSSQL_DRIVER'
69
72
  MSSQL_HOST = 'MSSQL_HOST'
@@ -78,6 +81,7 @@ class ConfigKey(StrEnum):
78
81
  MYSQL_PASSWORD = 'MYSQL_PASSWORD'
79
82
  MYSQL_PORT = 'MYSQL_PORT'
80
83
  MYSQL_USER = 'MYSQL_USER'
84
+ MYSQL_ALLOW_LOCAL_INFILE = 'MYSQL_ALLOW_LOCAL_INFILE'
81
85
 
82
86
  ORACLEDB_USER = 'ORACLEDB_USER'
83
87
  ORACLEDB_PASSWORD = 'ORACLEDB_PASSWORD'
@@ -338,6 +342,7 @@ class VerboseConfigKey(StrEnum):
338
342
  Config key headers for the verbose configuration file format.
339
343
  """
340
344
 
345
+ AIRTABLE = 'Airtable'
341
346
  ALGOLIA = 'Algolia'
342
347
  AWS = 'AWS'
343
348
  BIGQUERY = 'BigQuery'
@@ -356,6 +361,7 @@ class VerboseConfigKey(StrEnum):
356
361
 
357
362
  class ConfigFileLoader(BaseConfigLoader):
358
363
  KEY_MAP = {
364
+ ConfigKey.AIRTABLE_ACCESS_TOKEN: VerboseConfigKey.AIRTABLE,
359
365
  ConfigKey.ALGOLIA_APP_ID: (
360
366
  VerboseConfigKey.ALGOLIA, 'app_id'),
361
367
  ConfigKey.ALGOLIA_API_KEY: (
mage_ai/io/mssql.py CHANGED
@@ -14,6 +14,7 @@ from mage_ai.io.config import BaseConfigLoader, ConfigKey
14
14
  from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
15
15
  from mage_ai.io.export_utils import PandasTypes
16
16
  from mage_ai.io.sql import BaseSQL
17
+ from mage_ai.shared.hash import extract
17
18
  from mage_ai.shared.parsers import encode_complex
18
19
 
19
20
  MERGE_TABLE_SQL = '''MERGE {table_name} AS t
@@ -37,12 +38,14 @@ class MSSQL(BaseSQL):
37
38
  host: str,
38
39
  password: str,
39
40
  user: str,
41
+ authentication: str = None,
40
42
  schema: str = None,
41
43
  port: int = 1433,
42
44
  verbose: bool = True,
43
45
  **kwargs,
44
46
  ) -> None:
45
47
  super().__init__(
48
+ authentication=authentication,
46
49
  database=database,
47
50
  server=host,
48
51
  user=user,
@@ -60,15 +63,19 @@ class MSSQL(BaseSQL):
60
63
  database = self.settings['database']
61
64
  username = self.settings['user']
62
65
  password = self.settings['password']
63
- return (
64
- f'DRIVER={{{driver}}};'
65
- f'SERVER={server};'
66
- f'DATABASE={database};'
67
- f'UID={username};'
68
- f'PWD={password};'
69
- 'ENCRYPT=yes;'
70
- 'TrustServerCertificate=yes;'
71
- )
66
+ conn_opts = [
67
+ f'DRIVER={{{driver}}}',
68
+ f'SERVER={server}',
69
+ f'DATABASE={database}',
70
+ f'UID={username}',
71
+ f'PWD={password}',
72
+ 'ENCRYPT=yes',
73
+ 'TrustServerCertificate=yes',
74
+ ]
75
+ if self.settings.get('authentication'):
76
+ authentication = self.settings.get('authentication')
77
+ conn_opts.append(f'Authentication={authentication}')
78
+ return ';'.join(conn_opts)
72
79
 
73
80
  def default_schema(self) -> str:
74
81
  return self.settings.get('schema') or 'dbo'
@@ -197,9 +204,11 @@ class MSSQL(BaseSQL):
197
204
  TrustServerCertificate='yes',
198
205
  ),
199
206
  )
207
+ conn_kwargs = extract(kwargs, ['pool_size', 'max_overflow'])
200
208
  engine = create_engine(
201
209
  connection_url,
202
210
  fast_executemany=True,
211
+ **conn_kwargs,
203
212
  )
204
213
 
205
214
  unique_conflict_method = kwargs.get('unique_conflict_method')
@@ -242,6 +251,7 @@ class MSSQL(BaseSQL):
242
251
  method=merge_table,
243
252
  )
244
253
  return
254
+ sql_kwargs = extract(kwargs, ['chunksize', 'method'])
245
255
 
246
256
  df.to_sql(
247
257
  table_name,
@@ -249,6 +259,7 @@ class MSSQL(BaseSQL):
249
259
  schema=schema_name,
250
260
  if_exists=if_exists or ExportWritePolicy.REPLACE,
251
261
  index=False,
262
+ **sql_kwargs,
252
263
  )
253
264
 
254
265
  def get_type(self, column: Series, dtype: str) -> str:
@@ -313,6 +324,7 @@ class MSSQL(BaseSQL):
313
324
  @classmethod
314
325
  def with_config(cls, config: BaseConfigLoader) -> 'MSSQL':
315
326
  return cls(
327
+ authentication=config[ConfigKey.MSSQL_AUTHENTICATION],
316
328
  database=config[ConfigKey.MSSQL_DATABASE],
317
329
  schema=config[ConfigKey.MSSQL_SCHEMA],
318
330
  driver=config[ConfigKey.MSSQL_DRIVER],
mage_ai/io/mysql.py CHANGED
@@ -25,6 +25,7 @@ class MySQL(BaseSQL):
25
25
  password: str,
26
26
  user: str,
27
27
  port: int = 3306,
28
+ allow_local_infile: bool = False,
28
29
  verbose: bool = True,
29
30
  **kwargs,
30
31
  ) -> None:
@@ -35,18 +36,22 @@ class MySQL(BaseSQL):
35
36
  port=port or 3306,
36
37
  user=user,
37
38
  verbose=verbose,
39
+ allow_local_infile=allow_local_infile,
38
40
  **kwargs,
39
41
  )
40
42
 
41
43
  @classmethod
42
44
  def with_config(cls, config: BaseConfigLoader) -> 'MySQL':
43
- return cls(
45
+ conn_kwargs = dict(
44
46
  database=config[ConfigKey.MYSQL_DATABASE],
45
47
  host=config[ConfigKey.MYSQL_HOST],
46
48
  password=config[ConfigKey.MYSQL_PASSWORD],
47
49
  port=config[ConfigKey.MYSQL_PORT],
48
50
  user=config[ConfigKey.MYSQL_USER],
49
51
  )
52
+ if config[ConfigKey.MYSQL_ALLOW_LOCAL_INFILE] is not None:
53
+ conn_kwargs['allow_local_infile'] = config[ConfigKey.MYSQL_ALLOW_LOCAL_INFILE]
54
+ return cls(**conn_kwargs)
50
55
 
51
56
  def build_create_table_command(
52
57
  self,
mage_ai/io/postgres.py CHANGED
@@ -329,18 +329,21 @@ class Postgres(BaseSQL):
329
329
  return simplejson.dumps(
330
330
  val,
331
331
  default=encode_complex,
332
+ ensure_ascii=False,
332
333
  ignore_nan=True,
333
334
  )
334
335
  elif type(val) is list and len(val) >= 1 and type(val[0]) is dict:
335
336
  return simplejson.dumps(
336
337
  val,
337
338
  default=encode_complex,
339
+ ensure_ascii=False,
338
340
  ignore_nan=True,
339
341
  )
340
342
  elif not use_insert_command and type(val) is list:
341
343
  return self._clean_array_value(simplejson.dumps(
342
344
  val,
343
345
  default=encode_complex,
346
+ ensure_ascii=False,
344
347
  ignore_nan=True,
345
348
  ))
346
349
  return val
mage_ai/io/redshift.py CHANGED
@@ -5,6 +5,7 @@ from typing import Dict, Union
5
5
  from pandas import DataFrame
6
6
  from redshift_connector import connect
7
7
 
8
+ from mage_ai.data_preparation.models.block.sql.utils.shared import split_query_string
8
9
  from mage_ai.io.base import QUERY_ROW_LIMIT, ExportWritePolicy
9
10
  from mage_ai.io.config import BaseConfigLoader, ConfigKey
10
11
  from mage_ai.io.export_utils import clean_df_for_export, infer_dtypes
@@ -67,6 +68,18 @@ class Redshift(BaseSQL):
67
68
  with self.conn.cursor() as cur:
68
69
  cur.execute(query_string, **kwargs)
69
70
 
71
+ def execute_query_raw(self, query: str, **kwargs) -> None:
72
+ """
73
+ Overwrite execute query to process multiple queries in one string.
74
+ """
75
+ results = []
76
+ with self.conn.cursor() as cursor:
77
+ for query_string in split_query_string(query):
78
+ result = cursor.execute(query_string)
79
+ results.append(result)
80
+ self.conn.commit()
81
+ return results
82
+
70
83
  def load(
71
84
  self,
72
85
  query_string: str,
mage_ai/io/sql.py CHANGED
@@ -301,6 +301,7 @@ class BaseSQL(BaseSQLConnection):
301
301
  if_exists=if_exists,
302
302
  unique_conflict_method=unique_conflict_method,
303
303
  unique_constraints=unique_constraints,
304
+ **kwargs,
304
305
  )
305
306
  return
306
307
 
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ from functools import wraps
3
4
  from urllib.parse import parse_qs, quote_plus, urlparse
4
5
 
5
6
  import sqlalchemy
@@ -169,6 +170,25 @@ def safe_db_query(func):
169
170
  return func_with_rollback
170
171
 
171
172
 
173
+ def safe_db_query_async(func):
174
+ @wraps(func)
175
+ async def func_with_rollback_async(*args, **kwargs):
176
+ retry_count = 0
177
+ while True:
178
+ try:
179
+ return await func(*args, **kwargs)
180
+ except (
181
+ sqlalchemy.exc.OperationalError,
182
+ sqlalchemy.exc.PendingRollbackError,
183
+ sqlalchemy.exc.InternalError,
184
+ ) as e:
185
+ db_connection.session.rollback()
186
+ if retry_count >= DB_RETRY_COUNT:
187
+ raise e
188
+ retry_count += 1
189
+ return func_with_rollback_async
190
+
191
+
172
192
  logging.basicConfig()
173
193
 
174
194
  if is_debug() and not os.getenv('DISABLE_DATABASE_TERMINAL_OUTPUT'):
@@ -0,0 +1,47 @@
1
+ """Create GenericJob
2
+
3
+ Revision ID: 39d36f1dab73
4
+ Revises: 0227396a216c
5
+ Create Date: 2025-08-27 20:45:29.756869
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '39d36f1dab73'
14
+ down_revision = '0227396a216c'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ op.create_table('generic_job',
22
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
23
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
24
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
25
+ sa.Column('job_type', sa.Enum('CANCEL_PIPELINE_RUN', name='jobtype'), nullable=False),
26
+ sa.Column('status', sa.Enum('INITIAL', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', name='jobstatus'), nullable=True),
27
+ sa.Column('payload', sa.JSON(), nullable=True),
28
+ sa.Column('extra_metadata', sa.JSON(), nullable=True),
29
+ sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
30
+ sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
31
+ sa.Column('repo_path', sa.String(length=255), nullable=True),
32
+ sa.PrimaryKeyConstraint('id')
33
+ )
34
+ with op.batch_alter_table('generic_job', schema=None) as batch_op:
35
+ batch_op.create_index(batch_op.f('ix_generic_job_job_type'), ['job_type'], unique=False)
36
+ batch_op.create_index(batch_op.f('ix_generic_job_status'), ['status'], unique=False)
37
+ # ### end Alembic commands ###
38
+
39
+
40
+ def downgrade() -> None:
41
+ # ### commands auto generated by Alembic - please adjust! ###
42
+ with op.batch_alter_table('generic_job', schema=None) as batch_op:
43
+ batch_op.drop_index(batch_op.f('ix_generic_job_status'))
44
+ batch_op.drop_index(batch_op.f('ix_generic_job_job_type'))
45
+
46
+ op.drop_table('generic_job')
47
+ # ### end Alembic commands ###
@@ -882,4 +882,5 @@ class Oauth2AccessToken(BaseModel):
882
882
  def is_valid(self) -> bool:
883
883
  return self.token and \
884
884
  self.expires and \
885
- self.expires >= datetime.utcnow().replace(tzinfo=self.expires.tzinfo)
885
+ self.expires >= datetime.utcnow().replace(tzinfo=self.expires.tzinfo) and \
886
+ self.user is not None
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import collections
3
2
  import traceback
4
3
  import uuid
@@ -1400,10 +1399,8 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
1400
1399
 
1401
1400
  from mage_ai.usage_statistics.logger import UsageStatisticLogger
1402
1401
 
1403
- asyncio.run(
1404
- UsageStatisticLogger().pipeline_runs_impression(
1405
- lambda: self.query.filter(self.status == self.PipelineRunStatus.COMPLETED).count(),
1406
- )
1402
+ UsageStatisticLogger().pipeline_runs_impression_sync(
1403
+ lambda: self.query.filter(self.status == self.PipelineRunStatus.COMPLETED).count(),
1407
1404
  )
1408
1405
 
1409
1406
  @safe_db_query
@@ -1807,6 +1804,111 @@ class BlockRun(BlockRunProjectPlatformMixin, BaseModel):
1807
1804
  )
1808
1805
 
1809
1806
 
1807
+ class GenericJob(BaseModel):
1808
+ class JobStatus(StrEnum):
1809
+ INITIAL = 'initial'
1810
+ QUEUED = 'queued'
1811
+ RUNNING = 'running'
1812
+ COMPLETED = 'completed'
1813
+ FAILED = 'failed'
1814
+
1815
+ class JobType(StrEnum):
1816
+ CANCEL_PIPELINE_RUN = 'cancel_pipeline_run'
1817
+
1818
+ job_type = Column(Enum(JobType), nullable=False, index=True)
1819
+ status = Column(Enum(JobStatus), default=JobStatus.INITIAL, index=True)
1820
+ payload = Column(JSON) # Job-specific data (e.g., pipeline_run_id)
1821
+ extra_metadata = Column(JSON) # Additional metadata for the job
1822
+ started_at = Column(DateTime(timezone=True))
1823
+ completed_at = Column(DateTime(timezone=True))
1824
+ repo_path = Column(String(255))
1825
+
1826
+ @classproperty
1827
+ def repo_query(cls):
1828
+ return cls.query.filter(
1829
+ or_(
1830
+ GenericJob.repo_path == get_repo_path(),
1831
+ GenericJob.repo_path.is_(None),
1832
+ )
1833
+ )
1834
+
1835
+ @classmethod
1836
+ @safe_db_query
1837
+ def enqueue_cancel_pipeline_run(
1838
+ cls,
1839
+ pipeline_run_id: int,
1840
+ cancelled_block_run_ids: List,
1841
+ repo_path: str = None,
1842
+ ) -> 'GenericJob':
1843
+ """Enqueue a job to cancel a pipeline run."""
1844
+ if repo_path is None:
1845
+ repo_path = get_repo_path()
1846
+
1847
+ job = cls.create(
1848
+ job_type=cls.JobType.CANCEL_PIPELINE_RUN,
1849
+ payload=dict(
1850
+ pipeline_run_id=pipeline_run_id,
1851
+ cancelled_block_run_ids=cancelled_block_run_ids,
1852
+ ),
1853
+ repo_path=repo_path,
1854
+ status=cls.JobStatus.INITIAL
1855
+ )
1856
+ return job
1857
+
1858
+ @classmethod
1859
+ @safe_db_query
1860
+ def get_jobs_with_initial_status(
1861
+ cls,
1862
+ limit: int = 10,
1863
+ repo_path: str = None,
1864
+ ) -> List['GenericJob']:
1865
+ """Get up to N jobs with INITIAL status for processing."""
1866
+ query = cls.repo_query.filter(
1867
+ cls.status == cls.JobStatus.INITIAL,
1868
+ )
1869
+
1870
+ if repo_path:
1871
+ query = query.filter(cls.repo_path == repo_path)
1872
+
1873
+ jobs = query.order_by(cls.created_at.asc()).limit(limit).all()
1874
+
1875
+ return jobs
1876
+
1877
+ @safe_db_query
1878
+ def mark_completed(self):
1879
+ """Mark the job as completed successfully."""
1880
+ self.update(
1881
+ status=self.JobStatus.COMPLETED,
1882
+ completed_at=datetime.now(tz=pytz.UTC)
1883
+ )
1884
+
1885
+ @safe_db_query
1886
+ def mark_failed(self, metadata: dict = None):
1887
+ """Mark the job as failed."""
1888
+ self.update(
1889
+ status=self.JobStatus.FAILED,
1890
+ completed_at=datetime.now(tz=pytz.UTC),
1891
+ extra_metadata=metadata or {}
1892
+ )
1893
+
1894
+ @safe_db_query
1895
+ def mark_queued(self):
1896
+ """Mark the job as queued."""
1897
+ self.update(
1898
+ status=self.JobStatus.QUEUED,
1899
+ )
1900
+
1901
+ @safe_db_query
1902
+ def mark_running(self):
1903
+ """Mark the job as queued."""
1904
+ self.update(
1905
+ status=self.JobStatus.RUNNING,
1906
+ )
1907
+
1908
+ def __repr__(self):
1909
+ return f'GenericJob(id={self.id}, type={self.job_type}, status={self.status})'
1910
+
1911
+
1810
1912
  class EventMatcher(BaseModel):
1811
1913
  class EventType(StrEnum):
1812
1914
  AWS_EVENT = 'aws_event'
@@ -3,7 +3,8 @@ from typing import Optional
3
3
  from sqlalchemy import Column, String, Text, UniqueConstraint, or_
4
4
 
5
5
  from mage_ai.orchestration.db import safe_db_query
6
- from mage_ai.orchestration.db.models.base import BaseModel
6
+ from mage_ai.orchestration.db.models.base import BaseModel, classproperty
7
+ from mage_ai.settings.repo import get_repo_path
7
8
 
8
9
 
9
10
  class Secret(BaseModel):
@@ -15,6 +16,15 @@ class Secret(BaseModel):
15
16
  key_uuid = Column(String(255), nullable=True)
16
17
  __table_args__ = (UniqueConstraint('name', 'key_uuid', name='name_key_uuid_uc'),)
17
18
 
19
+ @classproperty
20
+ def repo_query(cls):
21
+ return cls.query.filter(
22
+ or_(
23
+ Secret.repo_name == get_repo_path(),
24
+ Secret.repo_name.is_(None),
25
+ )
26
+ )
27
+
18
28
  @classmethod
19
29
  @safe_db_query
20
30
  def get_secret(cls, name: str, key_uuid: str) -> Optional['Secret']: