mage-ai 0.9.76__py3-none-any.whl → 0.9.78__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 (166) hide show
  1. mage_ai/api/resources/GitFileResource.py +8 -0
  2. mage_ai/cli/main.py +6 -1
  3. mage_ai/data_preparation/executors/block_executor.py +8 -3
  4. mage_ai/data_preparation/executors/pipeline_executor.py +35 -19
  5. mage_ai/data_preparation/git/utils.py +2 -2
  6. mage_ai/data_preparation/logging/logger_manager.py +18 -2
  7. mage_ai/data_preparation/models/block/__init__.py +29 -22
  8. mage_ai/data_preparation/models/block/outputs.py +7 -1
  9. mage_ai/data_preparation/models/constants.py +2 -0
  10. mage_ai/data_preparation/storage/local_storage.py +4 -1
  11. mage_ai/io/config.py +1 -0
  12. mage_ai/io/mssql.py +16 -9
  13. mage_ai/io/postgres.py +3 -0
  14. mage_ai/orchestration/db/migrations/versions/39d36f1dab73_create_genericjob.py +47 -0
  15. mage_ai/orchestration/db/models/oauth.py +2 -1
  16. mage_ai/orchestration/db/models/schedules.py +105 -0
  17. mage_ai/orchestration/job_manager.py +19 -0
  18. mage_ai/orchestration/notification/sender.py +2 -2
  19. mage_ai/orchestration/pipeline_scheduler_original.py +146 -1
  20. mage_ai/orchestration/queue/config.py +11 -1
  21. mage_ai/orchestration/queue/process_queue.py +2 -0
  22. mage_ai/server/api/base.py +41 -0
  23. mage_ai/server/api/constants.py +1 -0
  24. mage_ai/server/constants.py +1 -1
  25. mage_ai/server/frontend_dist/404.html +2 -2
  26. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-3881267993576e2c.js +1 -0
  27. mage_ai/server/frontend_dist/_next/static/{qR0jauUABqPaFMjUsYeoG → yqYUo8S74SIsh2zOwDJBS}/_buildManifest.js +1 -1
  28. mage_ai/server/frontend_dist/block-layout.html +2 -2
  29. mage_ai/server/frontend_dist/compute.html +2 -2
  30. mage_ai/server/frontend_dist/files.html +2 -2
  31. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  32. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  33. mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
  34. mage_ai/server/frontend_dist/global-hooks.html +2 -2
  35. mage_ai/server/frontend_dist/index.html +2 -2
  36. mage_ai/server/frontend_dist/manage/files.html +2 -2
  37. mage_ai/server/frontend_dist/manage/overview.html +2 -2
  38. mage_ai/server/frontend_dist/manage/pipeline-runs.html +2 -2
  39. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  40. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  41. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  42. mage_ai/server/frontend_dist/manage/users.html +2 -2
  43. mage_ai/server/frontend_dist/manage.html +2 -2
  44. mage_ai/server/frontend_dist/oauth.html +3 -3
  45. mage_ai/server/frontend_dist/overview.html +2 -2
  46. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  47. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  48. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  49. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
  50. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  51. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  52. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  53. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  54. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  55. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  56. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  57. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  58. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  59. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  60. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  61. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  62. mage_ai/server/frontend_dist/pipelines.html +2 -2
  63. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
  64. mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
  65. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  66. mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
  67. mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
  68. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
  69. mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
  70. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  71. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
  72. mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
  73. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  74. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
  75. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  76. mage_ai/server/frontend_dist/settings.html +2 -2
  77. mage_ai/server/frontend_dist/sign-in.html +5 -5
  78. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  79. mage_ai/server/frontend_dist/templates.html +2 -2
  80. mage_ai/server/frontend_dist/terminal.html +2 -2
  81. mage_ai/server/frontend_dist/test.html +2 -2
  82. mage_ai/server/frontend_dist/triggers.html +2 -2
  83. mage_ai/server/frontend_dist/v2/canvas.html +2 -2
  84. mage_ai/server/frontend_dist/v2.html +2 -2
  85. mage_ai/server/frontend_dist/version-control.html +2 -2
  86. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  87. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-3881267993576e2c.js +1 -0
  88. mage_ai/server/frontend_dist_base_path_template/_next/static/{iCySon3_GCldnbC5U7C-s → xAmU_ZNdoOy8aaJdtbt_F}/_buildManifest.js +1 -1
  89. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  90. mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
  91. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  92. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  93. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  94. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
  95. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
  96. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  97. mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
  98. mage_ai/server/frontend_dist_base_path_template/manage/overview.html +2 -2
  99. mage_ai/server/frontend_dist_base_path_template/manage/pipeline-runs.html +2 -2
  100. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  101. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  102. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  103. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  104. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  105. mage_ai/server/frontend_dist_base_path_template/oauth.html +3 -3
  106. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  107. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  108. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  109. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  110. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
  111. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  112. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  113. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  114. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  115. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  116. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  117. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  118. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  119. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  120. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  121. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  122. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  123. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  124. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
  125. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
  126. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  127. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
  128. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
  129. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
  130. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
  131. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  132. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
  133. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
  134. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  135. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
  136. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  137. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  138. mage_ai/server/frontend_dist_base_path_template/sign-in.html +5 -5
  139. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  140. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  141. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  142. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  143. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  144. mage_ai/server/frontend_dist_base_path_template/v2/canvas.html +2 -2
  145. mage_ai/server/frontend_dist_base_path_template/v2.html +2 -2
  146. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  147. mage_ai/server/scheduler_manager.py +2 -0
  148. mage_ai/server/terminal_server.py +3 -0
  149. mage_ai/settings/server.py +3 -1
  150. mage_ai/streaming/sources/kafka.py +2 -1
  151. mage_ai/tests/data_preparation/executors/test_block_executor.py +3 -3
  152. mage_ai/tests/data_preparation/logging/test_logger_manager.py +24 -5
  153. mage_ai/tests/data_preparation/models/test_variable.py +2 -0
  154. mage_ai/tests/io/create_table/test_postgresql.py +3 -2
  155. mage_ai/tests/orchestration/notification/test_sender.py +5 -1
  156. mage_ai/tests/streaming/sources/test_kafka.py +2 -2
  157. {mage_ai-0.9.76.dist-info → mage_ai-0.9.78.dist-info}/METADATA +72 -107
  158. {mage_ai-0.9.76.dist-info → mage_ai-0.9.78.dist-info}/RECORD +164 -163
  159. {mage_ai-0.9.76.dist-info → mage_ai-0.9.78.dist-info}/WHEEL +1 -1
  160. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-432da20df91511fb.js +0 -1
  161. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-432da20df91511fb.js +0 -1
  162. /mage_ai/server/frontend_dist/_next/static/{qR0jauUABqPaFMjUsYeoG → yqYUo8S74SIsh2zOwDJBS}/_ssgManifest.js +0 -0
  163. /mage_ai/server/frontend_dist_base_path_template/_next/static/{iCySon3_GCldnbC5U7C-s → xAmU_ZNdoOy8aaJdtbt_F}/_ssgManifest.js +0 -0
  164. {mage_ai-0.9.76.dist-info → mage_ai-0.9.78.dist-info}/entry_points.txt +0 -0
  165. {mage_ai-0.9.76.dist-info → mage_ai-0.9.78.dist-info}/licenses/LICENSE +0 -0
  166. {mage_ai-0.9.76.dist-info → mage_ai-0.9.78.dist-info}/top_level.txt +0 -0
@@ -32,6 +32,14 @@ class GitFileResource(GenericResource):
32
32
  pass
33
33
 
34
34
  file_path_absolute = os.path.join(git_manager.repo_path, file_path)
35
+
36
+ # Prevent path traversal by resolving the absolute path location
37
+ # and checking if it's within the repo
38
+ if not os.path.abspath(file_path_absolute).startswith(
39
+ os.path.abspath(git_manager.repo_path)
40
+ ):
41
+ raise Exception("Access denied: Attempted path traversal")
42
+
35
43
  file = File.from_path(file_path_absolute)
36
44
  if not file.exists():
37
45
  file = File.from_path(file_path_absolute, '')
mage_ai/cli/main.py CHANGED
@@ -194,7 +194,11 @@ def run(
194
194
  from mage_ai.orchestration.db.models.schedules import PipelineRun
195
195
  from mage_ai.orchestration.utils.git import log_git_sync, run_git_sync
196
196
  from mage_ai.server.logger import Logger
197
- from mage_ai.settings import SENTRY_DSN, SENTRY_TRACES_SAMPLE_RATE
197
+ from mage_ai.settings import (
198
+ SENTRY_DSN,
199
+ SENTRY_SERVER_NAME,
200
+ SENTRY_TRACES_SAMPLE_RATE,
201
+ )
198
202
  from mage_ai.shared.hash import merge_dict
199
203
 
200
204
  logger = Logger().new_server_logger(__name__)
@@ -204,6 +208,7 @@ def run(
204
208
  sentry_sdk.init(
205
209
  sentry_dsn,
206
210
  traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
211
+ server_name=SENTRY_SERVER_NAME,
207
212
  )
208
213
  (enable_new_relic, application) = initialize_new_relic()
209
214
 
@@ -693,7 +693,7 @@ class BlockExecutor:
693
693
  ),
694
694
  tags=tags,
695
695
  )
696
- self._execute_callback(
696
+ self.execute_callback(
697
697
  'on_failure',
698
698
  block_run_id=block_run_id,
699
699
  callback_kwargs=dict(__error=error, retry=self.retry_metadata),
@@ -748,7 +748,7 @@ class BlockExecutor:
748
748
  # success callback because this isn’t the last data integration block that needs
749
749
  # to run.
750
750
  if not data_integration_metadata or is_original_block:
751
- self._execute_callback(
751
+ self.execute_callback(
752
752
  'on_success',
753
753
  block_run_id=block_run_id,
754
754
  callback_kwargs=dict(retry=self.retry_metadata),
@@ -1253,7 +1253,7 @@ class BlockExecutor:
1253
1253
 
1254
1254
  return result
1255
1255
 
1256
- def _execute_callback(
1256
+ def execute_callback(
1257
1257
  self,
1258
1258
  callback: str,
1259
1259
  global_vars: Dict,
@@ -1275,6 +1275,11 @@ class BlockExecutor:
1275
1275
  dynamic_block_index: Index of the dynamic block.
1276
1276
  dynamic_upstream_block_uuids: List of UUIDs of the dynamic upstream blocks.
1277
1277
  """
1278
+ if logging_tags is None:
1279
+ logging_tags = self.build_tags(
1280
+ block_run_id=block_run_id,
1281
+ pipeline_run_id=pipeline_run.id if pipeline_run is not None else None,
1282
+ )
1278
1283
  upstream_block_uuids_override = []
1279
1284
  if is_dynamic_block_child(self.block):
1280
1285
  if not self.block_run and block_run_id:
@@ -10,10 +10,13 @@ from mage_ai.data_preparation.logging.logger import DictLogger
10
10
  from mage_ai.data_preparation.logging.logger_manager_factory import LoggerManagerFactory
11
11
  from mage_ai.data_preparation.models.pipeline import Pipeline
12
12
  from mage_ai.orchestration.db.models.schedules import BlockRun, PipelineRun
13
+ from mage_ai.server.logger import Logger
13
14
  from mage_ai.shared.hash import merge_dict
14
15
  from mage_ai.usage_statistics.constants import EventNameType, EventObjectType
15
16
  from mage_ai.usage_statistics.logger import UsageStatisticLogger
16
17
 
18
+ logger = Logger().new_server_logger(__name__)
19
+
17
20
 
18
21
  class PipelineExecutor:
19
22
  def __init__(self, pipeline: Pipeline, execution_partition: str = None):
@@ -53,25 +56,38 @@ class PipelineExecutor:
53
56
  update_status (bool): Whether to update the execution status.
54
57
  **kwargs: Additional keyword arguments.
55
58
  """
56
- if pipeline_run_id is None:
57
- # Execute the pipeline without block runs
58
- asyncio.run(self.pipeline.execute(
59
- analyze_outputs=analyze_outputs,
60
- global_vars=global_vars,
61
- run_sensors=run_sensors,
62
- run_tests=run_tests,
63
- update_status=update_status,
64
- ))
65
- else:
66
- # Supported pipeline types: Standard batch pipeline
67
- pipeline_run = PipelineRun.query.get(pipeline_run_id)
68
- if pipeline_run.status != PipelineRun.PipelineRunStatus.RUNNING:
69
- return
70
- asyncio.run(self.__run_blocks(
71
- pipeline_run,
72
- allow_blocks_to_fail=allow_blocks_to_fail,
73
- global_vars=global_vars,
74
- ))
59
+ # Create the async task to execute
60
+ async def _execute_task():
61
+ if pipeline_run_id is None:
62
+ # Execute the pipeline without block runs
63
+ await self.pipeline.execute(
64
+ analyze_outputs=analyze_outputs,
65
+ global_vars=global_vars,
66
+ run_sensors=run_sensors,
67
+ run_tests=run_tests,
68
+ update_status=update_status,
69
+ )
70
+ else:
71
+ # Supported pipeline types: Standard batch pipeline
72
+ pipeline_run = PipelineRun.query.get(pipeline_run_id)
73
+ if pipeline_run.status != PipelineRun.PipelineRunStatus.RUNNING:
74
+ return
75
+ await self.__run_blocks(
76
+ pipeline_run,
77
+ allow_blocks_to_fail=allow_blocks_to_fail,
78
+ global_vars=global_vars,
79
+ )
80
+ # Execute the task based on current context
81
+ try:
82
+ loop = asyncio.get_running_loop()
83
+ logger.info(f'[PipelineExecutor] Found running loop {loop}')
84
+ # We're in an async context, use create_task
85
+ task = asyncio.create_task(_execute_task())
86
+ loop.run_until_complete(task)
87
+ except RuntimeError:
88
+ # No running loop, safe to use asyncio.run
89
+ logger.info('[PipelineExecutor] No running loop, using asyncio.run')
90
+ asyncio.run(_execute_task())
75
91
 
76
92
  self.logger_manager.output_logs_to_destination()
77
93
 
@@ -215,7 +215,7 @@ def check_connection(git, remote_url: str) -> None:
215
215
 
216
216
 
217
217
  async def check_connection_async(git, remote_name: str) -> None:
218
- proc = git.ls_remote(remote_name, as_process=True)
218
+ proc = git.ls_remote(remote_name, 'HEAD', as_process=True)
219
219
 
220
220
  await poll_process_with_timeout(
221
221
  proc,
@@ -228,7 +228,7 @@ async def check_connection_async(git, remote_name: str) -> None:
228
228
 
229
229
 
230
230
  async def validate_authentication_for_remote_url(git, remote_url: str) -> None:
231
- proc = git.ls_remote(remote_url, as_process=True)
231
+ proc = git.ls_remote(remote_url, 'HEAD', as_process=True)
232
232
 
233
233
  await poll_process_with_timeout(
234
234
  proc,
@@ -139,15 +139,31 @@ class LoggerManager:
139
139
  str_to_timedelta(log_retention_period)).strftime(
140
140
  format='%Y%m%dT%H%M%S')
141
141
 
142
+ from mage_ai.data_preparation.models.constants import PipelineType
143
+ from mage_ai.data_preparation.models.pipeline import Pipeline
142
144
  if self.pipeline_uuid is None:
143
- from mage_ai.data_preparation.models.pipeline import Pipeline
144
-
145
145
  pipeline_uuids = Pipeline.get_all_pipelines(self.repo_path)
146
146
  else:
147
147
  pipeline_uuids = [self.pipeline_uuid]
148
148
 
149
149
  for pipeline_uuid in pipeline_uuids:
150
150
  print(f'Removing old logs from pipeline {pipeline_uuid}')
151
+ # Skip cleaning up logs for streaming pipelines
152
+ skip_clean_up_logs = False
153
+ try:
154
+ p = Pipeline.get(pipeline_uuid)
155
+ if p.type == PipelineType.STREAMING:
156
+ print(f'Pipeline {pipeline_uuid} is a Streaming pipeline. '
157
+ 'Skip cleaning up logs.')
158
+ skip_clean_up_logs = True
159
+ except Exception as e:
160
+ print(f'Fail to load pipeline {pipeline_uuid} due to error {e}. '
161
+ 'Skip cleaning up logs.')
162
+ skip_clean_up_logs = True
163
+
164
+ if skip_clean_up_logs:
165
+ continue
166
+
151
167
  pipeline_log_path = self.get_log_filepath_prefix(pipeline_uuid=pipeline_uuid)
152
168
  dirs = self.storage.listdir(pipeline_log_path)
153
169
  for dirname in dirs:
@@ -4464,29 +4464,36 @@ class CallbackBlock(AddonBlock):
4464
4464
  elif 'on_success' == callback:
4465
4465
  callback_functions_legacy = success_functions
4466
4466
  callback_status = CallbackStatus.SUCCESS
4467
+ elif 'on_cancelled' == callback:
4468
+ callback_functions_legacy = []
4469
+ callback_status = CallbackStatus.CANCELLED
4467
4470
 
4468
- # Fetch input variables
4469
- input_vars, kwargs_vars, upstream_block_uuids = self.fetch_input_variables(
4470
- None,
4471
- dynamic_block_index=dynamic_block_index,
4472
- dynamic_block_indexes=dynamic_block_indexes,
4473
- dynamic_upstream_block_uuids=dynamic_upstream_block_uuids,
4474
- execution_partition=execution_partition,
4475
- from_notebook=from_notebook,
4476
- global_vars=global_vars,
4477
- metadata=metadata,
4478
- upstream_block_uuids=[parent_block.uuid] if parent_block else None,
4479
- upstream_block_uuids_override=upstream_block_uuids_override,
4480
- )
4481
-
4482
- # Copied logic from the method self.execute_block
4483
- outputs_from_input_vars = {}
4484
- upstream_block_uuids_length = len(upstream_block_uuids)
4485
- for idx, input_var in enumerate(input_vars):
4486
- if idx < upstream_block_uuids_length:
4487
- upstream_block_uuid = upstream_block_uuids[idx]
4488
- outputs_from_input_vars[upstream_block_uuid] = input_var
4489
- outputs_from_input_vars[f'df_{idx + 1}'] = input_var
4471
+ if callback_status in [CallbackStatus.FAILURE, CallbackStatus.SUCCESS]:
4472
+ # Fetch input variables
4473
+ input_vars, kwargs_vars, upstream_block_uuids = self.fetch_input_variables(
4474
+ None,
4475
+ dynamic_block_index=dynamic_block_index,
4476
+ dynamic_block_indexes=dynamic_block_indexes,
4477
+ dynamic_upstream_block_uuids=dynamic_upstream_block_uuids,
4478
+ execution_partition=execution_partition,
4479
+ from_notebook=from_notebook,
4480
+ global_vars=global_vars,
4481
+ metadata=metadata,
4482
+ upstream_block_uuids=[parent_block.uuid] if parent_block else None,
4483
+ upstream_block_uuids_override=upstream_block_uuids_override,
4484
+ )
4485
+ # Copied logic from the method self.execute_block
4486
+ outputs_from_input_vars = {}
4487
+ upstream_block_uuids_length = len(upstream_block_uuids)
4488
+ for idx, input_var in enumerate(input_vars):
4489
+ if idx < upstream_block_uuids_length:
4490
+ upstream_block_uuid = upstream_block_uuids[idx]
4491
+ outputs_from_input_vars[upstream_block_uuid] = input_var
4492
+ outputs_from_input_vars[f'df_{idx + 1}'] = input_var
4493
+ else:
4494
+ input_vars = []
4495
+ kwargs_vars = []
4496
+ upstream_block_uuids = []
4490
4497
 
4491
4498
  global_vars_copy = global_vars.copy()
4492
4499
  for kwargs_var in kwargs_vars:
@@ -343,7 +343,13 @@ def format_output_data(
343
343
  columns=columns_to_display,
344
344
  rows=[
345
345
  list(row.values())
346
- for row in json.loads(data[columns_to_display].write_json(row_oriented=True))
346
+ for row in json.loads(
347
+ simplejson.dumps(
348
+ data[columns_to_display].to_dicts(),
349
+ default=encode_complex,
350
+ ignore_nan=True,
351
+ )
352
+ )
347
353
  ],
348
354
  ),
349
355
  resource_usage=resource_usage,
@@ -82,6 +82,7 @@ class BlockColor(StrEnum):
82
82
 
83
83
 
84
84
  class CallbackStatus(StrEnum):
85
+ CANCELLED = 'cancelled'
85
86
  FAILURE = 'failure'
86
87
  SUCCESS = 'success'
87
88
 
@@ -126,6 +127,7 @@ BLOCK_LANGUAGE_TO_FILE_EXTENSION = {
126
127
 
127
128
 
128
129
  CALLBACK_STATUSES = [
130
+ CallbackStatus.CANCELLED,
129
131
  CallbackStatus.FAILURE,
130
132
  CallbackStatus.SUCCESS,
131
133
  ]
@@ -121,7 +121,10 @@ class LocalStorage(BaseStorage):
121
121
  return pd.read_parquet(file_path, engine='pyarrow')
122
122
 
123
123
  def read_polars_parquet(self, file_path: str, **kwargs) -> pl.DataFrame:
124
- return pl.read_parquet(file_path, use_pyarrow=True)
124
+ try:
125
+ return pl.read_parquet(file_path, use_pyarrow=True)
126
+ except Exception:
127
+ return pl.read_parquet(file_path)
125
128
 
126
129
  def write_csv(self, df: pd.DataFrame, file_path: str) -> None:
127
130
  File.create_parent_directories(file_path)
mage_ai/io/config.py CHANGED
@@ -66,6 +66,7 @@ class ConfigKey(StrEnum):
66
66
  MONGODB_PORT = 'MONGODB_PORT'
67
67
  MONGODB_USER = 'MONGODB_USER'
68
68
 
69
+ MSSQL_AUTHENTICATION = 'MSSQL_AUTHENTICATION'
69
70
  MSSQL_DATABASE = 'MSSQL_DATABASE'
70
71
  MSSQL_DRIVER = 'MSSQL_DRIVER'
71
72
  MSSQL_HOST = 'MSSQL_HOST'
mage_ai/io/mssql.py CHANGED
@@ -38,12 +38,14 @@ class MSSQL(BaseSQL):
38
38
  host: str,
39
39
  password: str,
40
40
  user: str,
41
+ authentication: str = None,
41
42
  schema: str = None,
42
43
  port: int = 1433,
43
44
  verbose: bool = True,
44
45
  **kwargs,
45
46
  ) -> None:
46
47
  super().__init__(
48
+ authentication=authentication,
47
49
  database=database,
48
50
  server=host,
49
51
  user=user,
@@ -61,15 +63,19 @@ class MSSQL(BaseSQL):
61
63
  database = self.settings['database']
62
64
  username = self.settings['user']
63
65
  password = self.settings['password']
64
- return (
65
- f'DRIVER={{{driver}}};'
66
- f'SERVER={server};'
67
- f'DATABASE={database};'
68
- f'UID={username};'
69
- f'PWD={password};'
70
- 'ENCRYPT=yes;'
71
- 'TrustServerCertificate=yes;'
72
- )
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)
73
79
 
74
80
  def default_schema(self) -> str:
75
81
  return self.settings.get('schema') or 'dbo'
@@ -318,6 +324,7 @@ class MSSQL(BaseSQL):
318
324
  @classmethod
319
325
  def with_config(cls, config: BaseConfigLoader) -> 'MSSQL':
320
326
  return cls(
327
+ authentication=config[ConfigKey.MSSQL_AUTHENTICATION],
321
328
  database=config[ConfigKey.MSSQL_DATABASE],
322
329
  schema=config[ConfigKey.MSSQL_SCHEMA],
323
330
  driver=config[ConfigKey.MSSQL_DRIVER],
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
@@ -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
@@ -1804,6 +1804,111 @@ class BlockRun(BlockRunProjectPlatformMixin, BaseModel):
1804
1804
  )
1805
1805
 
1806
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
+
1807
1912
  class EventMatcher(BaseModel):
1808
1913
  class EventType(StrEnum):
1809
1914
  AWS_EVENT = 'aws_event'
@@ -8,6 +8,7 @@ class JobType(StrEnum):
8
8
  BLOCK_RUN = 'block_run'
9
9
  PIPELINE_RUN = 'pipeline_run'
10
10
  INTEGRATION_STREAM = 'integration_stream'
11
+ GENERIC_JOB = 'generic_job'
11
12
 
12
13
 
13
14
  class JobManager:
@@ -62,6 +63,19 @@ class JobManager:
62
63
  logging_tags=logging_tags,
63
64
  )
64
65
 
66
+ def has_generic_job(
67
+ self,
68
+ generic_job_id: int,
69
+ logger=None,
70
+ logging_tags: Dict = None,
71
+ ) -> bool:
72
+ job_id = self.__job_id(JobType.GENERIC_JOB, generic_job_id)
73
+ return self.queue.has_job(
74
+ job_id,
75
+ logger=logger,
76
+ logging_tags=logging_tags,
77
+ )
78
+
65
79
  def kill_block_run_job(self, block_run_id):
66
80
  print(f'Kill block run id: {block_run_id}')
67
81
  job_id = self.__job_id(JobType.BLOCK_RUN, block_run_id)
@@ -78,6 +92,11 @@ class JobManager:
78
92
  job_id = self.__job_id(JobType.INTEGRATION_STREAM, id)
79
93
  return self.queue.kill_job(job_id)
80
94
 
95
+ def kill_generic_job(self, generic_job_id):
96
+ print(f'Kill generic job id: {generic_job_id}')
97
+ job_id = self.__job_id(JobType.GENERIC_JOB, generic_job_id)
98
+ return self.queue.kill_job(job_id)
99
+
81
100
  def start(self):
82
101
  self.queue.start()
83
102
 
@@ -69,7 +69,7 @@ class NotificationSender:
69
69
 
70
70
  if self.config.teams_config is not None and self.config.teams_config.is_valid:
71
71
  try:
72
- send_teams_message(self.config.teams_config, summary)
72
+ send_teams_message(self.config.teams_config, summary, title)
73
73
  except Exception:
74
74
  traceback.print_exc()
75
75
 
@@ -87,7 +87,7 @@ class NotificationSender:
87
87
 
88
88
  if self.config.google_chat_config is not None and self.config.google_chat_config.is_valid:
89
89
  try:
90
- send_google_chat_message(self.config.google_chat_config, summary)
90
+ send_google_chat_message(self.config.google_chat_config, summary, title)
91
91
  except Exception:
92
92
  traceback.print_exc()
93
93