mage-ai 0.8.78__py3-none-any.whl → 0.8.80__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 (65) hide show
  1. mage_ai/api/policies/PipelineRunPolicy.py +1 -0
  2. mage_ai/api/policies/StatusPolicy.py +4 -2
  3. mage_ai/api/presenters/StatusPresenter.py +1 -0
  4. mage_ai/api/resources/LogResource.py +12 -3
  5. mage_ai/api/resources/StatusResource.py +2 -0
  6. mage_ai/data_preparation/executors/streaming_pipeline_executor.py +73 -57
  7. mage_ai/data_preparation/models/block/__init__.py +30 -7
  8. mage_ai/data_preparation/models/constants.py +2 -0
  9. mage_ai/data_preparation/models/widget/charts.py +23 -4
  10. mage_ai/orchestration/db/models/schedules.py +2 -0
  11. mage_ai/orchestration/notification/sender.py +18 -5
  12. mage_ai/orchestration/pipeline_scheduler.py +26 -9
  13. mage_ai/server/constants.py +1 -1
  14. mage_ai/server/frontend_dist/404.html +2 -2
  15. mage_ai/server/frontend_dist/404.html.html +2 -2
  16. mage_ai/server/frontend_dist/_next/static/{m8Ltx9sPofwrShNBHkhe- → K62oaHK5x3k16vVxdvIWf}/_buildManifest.js +1 -1
  17. mage_ai/server/frontend_dist/_next/static/chunks/5682-c0d87b28bf381aae.js +1 -0
  18. mage_ai/server/frontend_dist/_next/static/chunks/8312-71137409aea5d028.js +1 -0
  19. mage_ai/server/frontend_dist/_next/static/chunks/8957-6edafc5a2521efdf.js +1 -0
  20. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-d90d32812b2be89e.js +1 -0
  21. mage_ai/server/frontend_dist/index.html +2 -2
  22. mage_ai/server/frontend_dist/manage.html +2 -2
  23. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  24. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  25. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  26. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  27. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  28. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  29. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  30. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  31. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  32. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  33. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  34. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  35. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  36. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  37. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  38. mage_ai/server/frontend_dist/pipelines.html +2 -2
  39. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  40. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  41. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  42. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  43. mage_ai/server/frontend_dist/settings.html +2 -2
  44. mage_ai/server/frontend_dist/sign-in.html +2 -2
  45. mage_ai/server/frontend_dist/terminal.html +2 -2
  46. mage_ai/server/frontend_dist/test.html +3 -3
  47. mage_ai/server/frontend_dist/triggers.html +2 -2
  48. mage_ai/server/kernel_output_parser.py +5 -0
  49. mage_ai/services/gcp/cloud_run/cloud_run.py +37 -18
  50. mage_ai/services/gcp/cloud_run/config.py +2 -1
  51. mage_ai/tests/orchestration/db/test_models.py +21 -10
  52. mage_ai/tests/orchestration/notification/test_sender.py +6 -4
  53. mage_ai/usage_statistics/logger.py +2 -0
  54. {mage_ai-0.8.78.dist-info → mage_ai-0.8.80.dist-info}/METADATA +1 -1
  55. {mage_ai-0.8.78.dist-info → mage_ai-0.8.80.dist-info}/RECORD +61 -61
  56. mage_ai/server/frontend_dist/_next/static/chunks/5682-83f92c7d6ec974f5.js +0 -1
  57. mage_ai/server/frontend_dist/_next/static/chunks/8312-3bb169298804c401.js +0 -1
  58. mage_ai/server/frontend_dist/_next/static/chunks/8957-0c39a705c8215858.js +0 -1
  59. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-22d76e8fa386ba37.js +0 -1
  60. /mage_ai/server/frontend_dist/_next/static/{m8Ltx9sPofwrShNBHkhe- → K62oaHK5x3k16vVxdvIWf}/_middlewareManifest.js +0 -0
  61. /mage_ai/server/frontend_dist/_next/static/{m8Ltx9sPofwrShNBHkhe- → K62oaHK5x3k16vVxdvIWf}/_ssgManifest.js +0 -0
  62. {mage_ai-0.8.78.dist-info → mage_ai-0.8.80.dist-info}/LICENSE +0 -0
  63. {mage_ai-0.8.78.dist-info → mage_ai-0.8.80.dist-info}/WHEEL +0 -0
  64. {mage_ai-0.8.78.dist-info → mage_ai-0.8.80.dist-info}/entry_points.txt +0 -0
  65. {mage_ai-0.8.78.dist-info → mage_ai-0.8.80.dist-info}/top_level.txt +0 -0
@@ -51,6 +51,7 @@ PipelineRunPolicy.allow_read(PipelineRunPresenter.default_attributes + [], scope
51
51
 
52
52
  PipelineRunPolicy.allow_write([
53
53
  'backfill_id',
54
+ 'event_variables',
54
55
  'execution_date',
55
56
  'pipeline_schedule_id',
56
57
  'pipeline_uuid',
@@ -12,10 +12,12 @@ StatusPolicy.allow_actions([
12
12
  constants.LIST,
13
13
  ], scopes=[
14
14
  OauthScope.CLIENT_PRIVATE,
15
- ], condition=lambda policy: policy.has_at_least_viewer_role())
15
+ OauthScope.CLIENT_PUBLIC,
16
+ ])
16
17
 
17
18
  StatusPolicy.allow_read(StatusPresenter.default_attributes, scopes=[
18
19
  OauthScope.CLIENT_PRIVATE,
20
+ OauthScope.CLIENT_PUBLIC,
19
21
  ], on_action=[
20
22
  constants.LIST,
21
- ], condition=lambda policy: policy.has_at_least_viewer_role())
23
+ ])
@@ -4,6 +4,7 @@ from mage_ai.api.presenters.BasePresenter import BasePresenter
4
4
  class StatusPresenter(BasePresenter):
5
5
  default_attributes = [
6
6
  'is_instance_manager',
7
+ 'max_print_output_lines',
7
8
  'repo_path',
8
9
  'scheduler_status',
9
10
  'instance_type',
@@ -1,12 +1,18 @@
1
1
  from datetime import datetime
2
+ from typing import Dict, List
3
+
4
+ from sqlalchemy.orm import aliased
5
+
2
6
  from mage_ai.api.errors import ApiError
3
7
  from mage_ai.api.operations.constants import META_KEY_LIMIT
4
8
  from mage_ai.api.resources.GenericResource import GenericResource
5
9
  from mage_ai.data_preparation.models.pipeline import Pipeline
6
10
  from mage_ai.orchestration.db import safe_db_query
7
- from mage_ai.orchestration.db.models.schedules import BlockRun, PipelineRun, PipelineSchedule
8
- from sqlalchemy.orm import aliased
9
- from typing import Dict, List
11
+ from mage_ai.orchestration.db.models.schedules import (
12
+ BlockRun,
13
+ PipelineRun,
14
+ PipelineSchedule,
15
+ )
10
16
 
11
17
  MAX_LOG_FILES = 20
12
18
 
@@ -96,6 +102,7 @@ class LogResource(GenericResource):
96
102
  a.pipeline_schedule_id,
97
103
  a.pipeline_schedule_id,
98
104
  a.pipeline_uuid,
105
+ a.variables,
99
106
  ]
100
107
 
101
108
  total_pipeline_run_log_count = 0
@@ -155,6 +162,7 @@ class LogResource(GenericResource):
155
162
  model.execution_date = row.execution_date
156
163
  model.pipeline_schedule_id = row.pipeline_schedule_id
157
164
  model.pipeline_uuid = row.pipeline_uuid
165
+ model.variables = row.variables
158
166
  logs = await model.logs_async()
159
167
  pipeline_log_file_path = logs.get('path')
160
168
  if pipeline_log_file_path not in processed_pipeline_run_log_files:
@@ -235,6 +243,7 @@ class LogResource(GenericResource):
235
243
  model.execution_date = row.execution_date
236
244
  model.pipeline_schedule_id = row.pipeline_schedule_id
237
245
  model.pipeline_uuid = row.pipeline_uuid
246
+ model.variables = row.variables
238
247
 
239
248
  model2 = BlockRun()
240
249
  model2.block_uuid = row.block_uuid
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
3
  from mage_ai.api.resources.GenericResource import GenericResource
4
+ from mage_ai.data_preparation.models.constants import MAX_PRINT_OUTPUT_LINES
4
5
  from mage_ai.data_preparation.repo_manager import (
5
6
  ProjectType,
6
7
  get_project_type,
@@ -53,6 +54,7 @@ class StatusResource(GenericResource):
53
54
  'scheduler_status': scheduler_manager.get_status(),
54
55
  'instance_type': instance_type,
55
56
  'disable_pipeline_edit_access': is_disable_pipeline_edit_access(),
57
+ 'max_print_output_lines': MAX_PRINT_OUTPUT_LINES,
56
58
  'require_user_authentication': REQUIRE_USER_AUTHENTICATION,
57
59
  }
58
60
  return self.build_result_set([status], user, **kwargs)
@@ -1,15 +1,18 @@
1
+ import asyncio
2
+ import copy
3
+ import logging
4
+ import os
1
5
  from contextlib import redirect_stderr, redirect_stdout
6
+ from typing import Callable, Dict, List, Union
7
+
8
+ import yaml
9
+
2
10
  from mage_ai.data_preparation.executors.pipeline_executor import PipelineExecutor
3
11
  from mage_ai.data_preparation.logging.logger import DictLogger
4
12
  from mage_ai.data_preparation.models.constants import BlockType
5
13
  from mage_ai.data_preparation.models.pipeline import Pipeline
6
14
  from mage_ai.data_preparation.shared.stream import StreamToLogger
7
15
  from mage_ai.shared.hash import merge_dict
8
- from typing import Callable, Dict, List, Union
9
- import asyncio
10
- import logging
11
- import os
12
- import yaml
13
16
 
14
17
 
15
18
  class StreamingPipelineExecutor(PipelineExecutor):
@@ -21,7 +24,9 @@ class StreamingPipelineExecutor(PipelineExecutor):
21
24
  def parse_and_validate_blocks(self):
22
25
  """
23
26
  Find the first valid streaming pipeline is in the structure:
24
- source -> transformer -> sink
27
+ source -> transformer1 -> sink2
28
+ -> transformer2 -> sink2
29
+ -> transformer3 -> sink3
25
30
  """
26
31
  blocks = self.pipeline.blocks_by_uuid.values()
27
32
  source_blocks = []
@@ -29,24 +34,23 @@ class StreamingPipelineExecutor(PipelineExecutor):
29
34
  transformer_blocks = []
30
35
  for b in blocks:
31
36
  if b.type == BlockType.DATA_LOADER:
37
+ # Data loader block should be root block
32
38
  if len(b.upstream_blocks or []) > 0:
33
39
  raise Exception(f'Data loader {b.uuid} can\'t have upstream blocks.')
34
- if len(b.downstream_blocks or []) != 1:
35
- raise Exception(f'Data loader {b.uuid} must have one transformer or data'
36
- ' exporter as the downstream block.')
40
+ if len(b.downstream_blocks or []) < 1:
41
+ raise Exception(f'Data loader {b.uuid} must have at least one transformer or'
42
+ ' data exporter as the downstream block.')
37
43
  source_blocks.append(b)
38
44
  if b.type == BlockType.DATA_EXPORTER:
45
+ # Data exporter block should be leaf block
39
46
  if len(b.downstream_blocks or []) > 0:
40
47
  raise Exception(f'Data expoter {b.uuid} can\'t have downstream blocks.')
41
48
  if len(b.upstream_blocks or []) != 1:
42
- raise Exception(f'Data loader {b.uuid} must have a transformer or data'
43
- ' exporter as the upstream block.')
49
+ raise Exception(f'Data exporter {b.uuid} must have a transformer or data'
50
+ ' loader as the upstream block.')
44
51
  sink_blocks.append(b)
45
52
  if b.type == BlockType.TRANSFORMER:
46
- if len(b.downstream_blocks or []) != 1:
47
- raise Exception(
48
- f'Transformer {b.uuid} should (only) have one downstream block.',
49
- )
53
+ # Each transformer block can only have one upstream block
50
54
  if len(b.upstream_blocks or []) != 1:
51
55
  raise Exception(f'Transformer {b.uuid} should (only) have one upstream block.')
52
56
  transformer_blocks.append(b)
@@ -54,15 +58,8 @@ class StreamingPipelineExecutor(PipelineExecutor):
54
58
  if len(source_blocks) != 1:
55
59
  raise Exception('Please provide (only) one data loader block as the source.')
56
60
 
57
- if len(transformer_blocks) > 1:
58
- raise Exception('Please provide no more than one transformer block.')
59
-
60
- if len(sink_blocks) != 1:
61
- raise Exception('Please provide (only) one data expoter block as the sink.')
62
-
63
61
  self.source_block = source_blocks[0]
64
- self.sink_block = sink_blocks[0]
65
- self.transformer_block = transformer_blocks[0] if len(transformer_blocks) > 0 else None
62
+ self.sink_blocks = sink_blocks
66
63
 
67
64
  def execute(
68
65
  self,
@@ -97,11 +94,10 @@ class StreamingPipelineExecutor(PipelineExecutor):
97
94
  raise e
98
95
 
99
96
  def __execute_in_python(self, build_block_output_stdout: Callable[..., object] = None):
97
+ from mage_ai.streaming.sinks.sink_factory import SinkFactory
100
98
  from mage_ai.streaming.sources.base import SourceConsumeMethod
101
99
  from mage_ai.streaming.sources.source_factory import SourceFactory
102
- from mage_ai.streaming.sinks.sink_factory import SinkFactory
103
100
  source_config = yaml.safe_load(self.source_block.content)
104
- sink_config = yaml.safe_load(self.sink_block.content)
105
101
  source = SourceFactory.get_source(
106
102
  source_config,
107
103
  checkpoint_path=os.path.join(
@@ -109,47 +105,67 @@ class StreamingPipelineExecutor(PipelineExecutor):
109
105
  'streaming_checkpoint',
110
106
  ),
111
107
  )
112
- sink = SinkFactory.get_sink(
113
- sink_config,
114
- buffer_path=os.path.join(
115
- self.pipeline.pipeline_variables_dir,
116
- 'buffer',
117
- ),
118
- )
108
+
109
+ sinks_by_uuid = dict()
110
+ for sink_block in self.sink_blocks:
111
+ sink_config = yaml.safe_load(sink_block.content)
112
+ sinks_by_uuid[sink_block.uuid] = SinkFactory.get_sink(
113
+ sink_config,
114
+ buffer_path=os.path.join(
115
+ self.pipeline.pipeline_variables_dir,
116
+ 'buffer',
117
+ ),
118
+ )
119
+
120
+ def handle_batch_events_recursively(curr_block, outputs_by_block: Dict, **kwargs):
121
+ curr_block_output = outputs_by_block[curr_block.uuid]
122
+ for downstream_block in curr_block.downstream_blocks:
123
+ if downstream_block.type == BlockType.TRANSFORMER:
124
+ execute_block_kwargs = dict(
125
+ global_vars=kwargs,
126
+ input_args=[copy.deepcopy(curr_block_output)],
127
+ logger=self.logger,
128
+ )
129
+ if build_block_output_stdout:
130
+ execute_block_kwargs['build_block_output_stdout'] = \
131
+ build_block_output_stdout
132
+ outputs_by_block[downstream_block.uuid] = \
133
+ downstream_block.execute_block(
134
+ **execute_block_kwargs,
135
+ )['output']
136
+ elif downstream_block.type == BlockType.DATA_EXPORTER:
137
+ sinks_by_uuid[downstream_block.uuid].batch_write(
138
+ copy.deepcopy(curr_block_output))
139
+ if downstream_block.downstream_blocks:
140
+ handle_batch_events_recursively(
141
+ downstream_block,
142
+ outputs_by_block,
143
+ **kwargs,
144
+ )
119
145
 
120
146
  def handle_batch_events(messages: List[Union[Dict, str]], **kwargs):
121
- if self.transformer_block is not None:
122
- execute_block_kwargs = dict(
123
- global_vars=kwargs,
124
- input_args=[messages],
125
- logger=self.logger,
126
- )
127
- if build_block_output_stdout:
128
- execute_block_kwargs['build_block_output_stdout'] = build_block_output_stdout
129
- messages = self.transformer_block.execute_block(
130
- **execute_block_kwargs,
131
- )['output']
132
- sink.batch_write(messages)
147
+ # Handle the events with DFS
148
+
149
+ outputs_by_block = dict()
150
+ outputs_by_block[self.source_block.uuid] = messages
151
+
152
+ handle_batch_events_recursively(self.source_block, outputs_by_block)
133
153
 
134
154
  async def handle_event_async(message, **kwargs):
135
- if self.transformer_block is not None:
136
- execute_block_kwargs = dict(
137
- global_vars=kwargs,
138
- input_args=[[message]],
139
- logger=self.logger,
140
- )
141
- if build_block_output_stdout:
142
- execute_block_kwargs['build_block_output_stdout'] = build_block_output_stdout
143
- messages = self.transformer_block.execute_block(
144
- **execute_block_kwargs,
145
- )['output']
146
- sink.batch_write(messages)
155
+ outputs_by_block = dict()
156
+ outputs_by_block[self.source_block.uuid] = [message]
157
+
158
+ handle_batch_events_recursively(self.source_block, outputs_by_block)
147
159
 
148
160
  # Long running method
149
161
  if source.consume_method == SourceConsumeMethod.BATCH_READ:
150
162
  source.batch_read(handler=handle_batch_events)
151
163
  elif source.consume_method == SourceConsumeMethod.READ_ASYNC:
152
- asyncio.run(source.read_async(handler=handle_event_async))
164
+ loop = asyncio.get_event_loop()
165
+ if loop is not None:
166
+ loop.run_until_complete(source.read_async(handler=handle_event_async))
167
+ else:
168
+ asyncio.run(source.read_async(handler=handle_event_async))
153
169
 
154
170
  def __excute_in_flink(self):
155
171
  """
@@ -227,9 +227,11 @@ class Block:
227
227
  status: BlockStatus = BlockStatus.NOT_EXECUTED,
228
228
  pipeline=None,
229
229
  language: BlockLanguage = BlockLanguage.PYTHON,
230
- configuration: Dict = dict(),
230
+ configuration: Dict = None,
231
231
  has_callback: bool = False,
232
232
  ):
233
+ if configuration is None:
234
+ configuration = dict()
233
235
  self.name = name or uuid
234
236
  self._uuid = uuid
235
237
  self.type = block_type
@@ -614,7 +616,7 @@ class Block:
614
616
  self,
615
617
  global_vars: Dict = None,
616
618
  logger: Logger = None,
617
- logging_tags: Dict = dict(),
619
+ logging_tags: Dict = None,
618
620
  **kwargs
619
621
  ):
620
622
  """
@@ -623,6 +625,9 @@ class Block:
623
625
  websocket as a way to test the code in the callback. To run a block in a pipeline
624
626
  run, use a BlockExecutor.
625
627
  """
628
+ if logging_tags is None:
629
+ logging_tags = dict()
630
+
626
631
  arr = []
627
632
  if self.callback_block:
628
633
  arr.append(self.callback_block)
@@ -643,6 +648,7 @@ class Block:
643
648
  global_vars=global_vars,
644
649
  logger=logger,
645
650
  logging_tags=logging_tags,
651
+ parent_block=self,
646
652
  )
647
653
  raise e
648
654
 
@@ -652,6 +658,7 @@ class Block:
652
658
  global_vars=global_vars,
653
659
  logger=logger,
654
660
  logging_tags=logging_tags,
661
+ parent_block=self,
655
662
  )
656
663
 
657
664
  return output
@@ -664,7 +671,7 @@ class Block:
664
671
  execution_partition: str = None,
665
672
  global_vars: Dict = None,
666
673
  logger: Logger = None,
667
- logging_tags: Dict = dict(),
674
+ logging_tags: Dict = None,
668
675
  run_all_blocks: bool = False,
669
676
  test_execution: bool = False,
670
677
  update_status: bool = True,
@@ -678,6 +685,9 @@ class Block:
678
685
  run_settings: Dict = None,
679
686
  **kwargs,
680
687
  ) -> Dict:
688
+ if logging_tags is None:
689
+ logging_tags = dict()
690
+
681
691
  try:
682
692
  if not run_all_blocks:
683
693
  not_executed_upstream_blocks = list(
@@ -873,7 +883,7 @@ class Block:
873
883
  execution_partition: str = None,
874
884
  input_args: List = None,
875
885
  logger: Logger = None,
876
- logging_tags: Dict = dict(),
886
+ logging_tags: Dict = None,
877
887
  global_vars: Dict = None,
878
888
  test_execution: bool = False,
879
889
  input_from_output: Dict = None,
@@ -883,6 +893,9 @@ class Block:
883
893
  run_settings: Dict = None,
884
894
  **kwargs,
885
895
  ) -> Dict:
896
+ if logging_tags is None:
897
+ logging_tags = dict()
898
+
886
899
  # Add pipeline uuid and block uuid to global_vars
887
900
  global_vars = merge_dict(
888
901
  global_vars or dict(),
@@ -951,7 +964,7 @@ class Block:
951
964
  execution_partition: str = None,
952
965
  input_vars: List = None,
953
966
  logger: Logger = None,
954
- logging_tags: Dict = dict(),
967
+ logging_tags: Dict = None,
955
968
  global_vars: Dict = None,
956
969
  test_execution: bool = False,
957
970
  input_from_output: Dict = None,
@@ -960,6 +973,9 @@ class Block:
960
973
  run_settings: Dict = None,
961
974
  **kwargs,
962
975
  ) -> List:
976
+ if logging_tags is None:
977
+ logging_tags = dict()
978
+
963
979
  decorated_functions = []
964
980
  test_functions = []
965
981
 
@@ -1531,13 +1547,18 @@ df = get_variable('{self.pipeline.uuid}', '{block_uuid}', 'df')
1531
1547
  build_block_output_stdout: Callable[..., object] = None,
1532
1548
  custom_code: str = None,
1533
1549
  execution_partition: str = None,
1534
- global_vars: Dict = {},
1550
+ global_vars: Dict = None,
1535
1551
  logger: Logger = None,
1536
- logging_tags: Dict = dict(),
1552
+ logging_tags: Dict = None,
1537
1553
  update_tests: bool = True,
1538
1554
  dynamic_block_uuid: str = None,
1539
1555
  from_notebook: bool = False,
1540
1556
  ) -> None:
1557
+ if global_vars is None:
1558
+ global_vars = dict()
1559
+ if logging_tags is None:
1560
+ logging_tags = dict()
1561
+
1541
1562
  self.dynamic_block_uuid = dynamic_block_uuid
1542
1563
 
1543
1564
  if self.pipeline \
@@ -2133,6 +2154,8 @@ class CallbackBlock(Block):
2133
2154
  pipeline_run=pipeline_run,
2134
2155
  ),
2135
2156
  )
2157
+ if parent_block:
2158
+ global_vars['parent_block_uuid'] = parent_block.uuid
2136
2159
 
2137
2160
  if parent_block and \
2138
2161
  parent_block.pipeline and \
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from enum import Enum
2
3
 
3
4
  PIPELINES_FOLDER = 'pipelines'
@@ -19,6 +20,7 @@ DATAFRAME_ANALYSIS_MAX_COLUMNS = 100
19
20
  DATAFRAME_SAMPLE_COUNT_PREVIEW = 10
20
21
  DATAFRAME_SAMPLE_COUNT = 1000
21
22
  DATAFRAME_SAMPLE_MAX_COLUMNS = 1000
23
+ MAX_PRINT_OUTPUT_LINES = int(os.getenv('MAX_PRINT_OUTPUT_LINES', 1000) or 1000)
22
24
  VARIABLE_DIR = '.variables'
23
25
  LOGS_DIR = '.logs'
24
26
 
@@ -82,10 +82,29 @@ def build_time_series_buckets(df, datetime_column, time_interval, metrics):
82
82
  return []
83
83
 
84
84
  datetimes = datetimes.unique()
85
- min_value_datetime = dateutil.parser.parse(datetimes.min())
86
- max_value_datetime = dateutil.parser.parse(datetimes.max())
87
-
88
- a, b = [dateutil.parser.parse(d) for d in sorted(datetimes)[:2]]
85
+ min_value_datetime = datetimes.min()
86
+ max_value_datetime = datetimes.max()
87
+
88
+ if type(min_value_datetime) is str:
89
+ min_value_datetime = dateutil.parser.parse(min_value_datetime)
90
+ if type(max_value_datetime) is str:
91
+ max_value_datetime = dateutil.parser.parse(max_value_datetime)
92
+
93
+ # If you manually convert the datetime column to a datetime, Pandas will use numpy.datetime64
94
+ # type. This type does not have the methods year, month, day, etc that is used down below.
95
+ datetimes_temp = []
96
+ for dt in datetimes:
97
+ if type(dt) is np.datetime64:
98
+ datetimes_temp.append(pd.to_datetime(dt.astype(datetime)).to_pydatetime())
99
+ else:
100
+ datetimes_temp.append(dt)
101
+ datetimes = datetimes_temp
102
+ if type(min_value_datetime) is np.datetime64:
103
+ min_value_datetime = pd.to_datetime(min_value_datetime.astype(datetime)).to_pydatetime()
104
+ if type(max_value_datetime) is np.datetime64:
105
+ max_value_datetime = pd.to_datetime(max_value_datetime.astype(datetime)).to_pydatetime()
106
+
107
+ a, b = [dateutil.parser.parse(d) if type(d) is str else d for d in sorted(datetimes)[:2]]
89
108
 
90
109
  year = min_value_datetime.year
91
110
  month = min_value_datetime.month
@@ -237,6 +237,8 @@ class PipelineRun(BaseModel):
237
237
 
238
238
  @property
239
239
  def execution_partition(self) -> str:
240
+ if self.variables and self.variables.get('execution_partition'):
241
+ return self.variables.get('execution_partition')
240
242
  if self.execution_date is None:
241
243
  return str(self.pipeline_schedule_id)
242
244
  else:
@@ -1,11 +1,12 @@
1
+ import os
2
+
1
3
  from mage_ai.orchestration.notification.config import AlertOn, NotificationConfig
2
4
  from mage_ai.services.email.email import send_email
5
+ from mage_ai.services.google_chat.google_chat import send_google_chat_message
3
6
  from mage_ai.services.opsgenie.opsgenie import send_opsgenie_alert
4
7
  from mage_ai.services.slack.slack import send_slack_message
5
8
  from mage_ai.services.teams.teams import send_teams_message
6
- from mage_ai.services.google_chat.google_chat import send_google_chat_message
7
9
  from mage_ai.settings import MAGE_PUBLIC_HOST
8
- import os
9
10
 
10
11
 
11
12
  class NotificationSender:
@@ -18,10 +19,17 @@ class NotificationSender:
18
19
  summary: str = None,
19
20
  details: str = None,
20
21
  ) -> None:
22
+ """Send messages to the notification channels.
23
+
24
+ Args:
25
+ title (str, optional): Short sentence, used as title (e.g. Email subject)
26
+ summary (str, optional): Mid-length sentences, used as the summary of the message.
27
+ details (str, optional): Long message, used as the body of the message (e.g. Email body)
28
+ """
21
29
  if summary is None:
22
30
  return
23
31
  if self.config.slack_config is not None and self.config.slack_config.is_valid:
24
- send_slack_message(self.config.slack_config, summary)
32
+ send_slack_message(self.config.slack_config, details or summary)
25
33
 
26
34
  if self.config.teams_config is not None and self.config.teams_config.is_valid:
27
35
  send_teams_message(self.config.teams_config, summary)
@@ -75,9 +83,14 @@ class NotificationSender:
75
83
  f'at execution time `{pipeline_run.execution_date}`.'
76
84
  )
77
85
  email_content = f'{message}\n'
78
- if os.getenv('ENV') != 'production':
86
+ if os.getenv('ENV') != 'production' or MAGE_PUBLIC_HOST != 'http://localhost:6789':
87
+ """
88
+ Include the URL for the following cases
89
+ 1. Dev environment: Use the default localhost as host in URL
90
+ 2. Production environment: If MAGE_PUBLIC_HOST is set, use it as host.
91
+ """
79
92
  email_content += f'Open {self.__pipeline_run_url(pipeline, pipeline_run)} '\
80
- 'to check pipeline run results and logs.'
93
+ 'to check pipeline run results and logs.'
81
94
  self.send(
82
95
  title=f'Failed to run Mage pipeline {pipeline.uuid}',
83
96
  summary=message,
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import traceback
2
3
  from datetime import datetime, timedelta
3
4
  from typing import Any, Dict, List, Tuple
@@ -274,7 +275,9 @@ class PipelineScheduler:
274
275
  tags=merge_dict(tags, dict(metrics=self.pipeline_run.metrics)),
275
276
  )
276
277
 
277
- def memory_usage_failure(self, tags: Dict = {}) -> None:
278
+ def memory_usage_failure(self, tags: Dict = None) -> None:
279
+ if tags is None:
280
+ tags = dict()
278
281
  msg = 'Memory usage across all pipeline runs has reached or exceeded the maximum '\
279
282
  f'limit of {int(MEMORY_USAGE_MAXIMUM * 100)}%.'
280
283
  self.logger.info(msg, tags=tags)
@@ -603,7 +606,8 @@ def run_integration_pipeline(
603
606
  while True:
604
607
  block_runs_in_order.append(
605
608
  find(
606
- lambda b: b.block_uuid == f'{current_block.uuid}:{tap_stream_id}:{idx}',
609
+ lambda b: b.block_uuid ==
610
+ f'{current_block.uuid}:{tap_stream_id}:{idx}', # noqa: B023
607
611
  all_block_runs,
608
612
  )
609
613
  )
@@ -616,11 +620,11 @@ def run_integration_pipeline(
616
620
  data_exporter_uuid = f'{data_exporter_block.uuid}:{tap_stream_id}:{idx}'
617
621
 
618
622
  data_loader_block_run = find(
619
- lambda b: b.block_uuid == data_loader_uuid,
623
+ lambda b: b.block_uuid == data_loader_uuid, # noqa: B023
620
624
  all_block_runs,
621
625
  )
622
626
  data_exporter_block_run = find(
623
- lambda b: b.block_uuid == data_exporter_uuid,
627
+ lambda b: b.block_uuid == data_exporter_uuid, # noqa: B023
624
628
  block_runs_for_stream,
625
629
  )
626
630
  if not data_loader_block_run or not data_exporter_block_run:
@@ -653,7 +657,7 @@ def run_integration_pipeline(
653
657
  block_runs_and_configs = block_runs_and_configs[1:]
654
658
 
655
659
  block_failed = False
656
- for idx2, tup in enumerate(block_runs_and_configs):
660
+ for _, tup in enumerate(block_runs_and_configs):
657
661
  block_run, template_runtime_configuration = tup
658
662
 
659
663
  tags_updated = merge_dict(tags, dict(
@@ -813,7 +817,7 @@ def run_block(
813
817
  block_grandparent_uuid,
814
818
  )
815
819
 
816
- for idx, value in enumerate(values):
820
+ for idx, _ in enumerate(values):
817
821
  if idx < len(block_metadata):
818
822
  metadata = block_metadata[idx].copy()
819
823
  else:
@@ -880,8 +884,10 @@ def run_pipeline(pipeline_run_id, variables, tags):
880
884
  def configure_pipeline_run_payload(
881
885
  pipeline_schedule: PipelineSchedule,
882
886
  pipeline_type: PipelineType,
883
- payload: Dict = {},
887
+ payload: Dict = None,
884
888
  ) -> Tuple[Dict, bool]:
889
+ if payload is None:
890
+ payload = dict()
885
891
  if 'variables' not in payload:
886
892
  payload['variables'] = {}
887
893
 
@@ -893,6 +899,13 @@ def configure_pipeline_run_payload(
893
899
  elif not isinstance(execution_date, datetime):
894
900
  payload['execution_date'] = datetime.fromisoformat(execution_date)
895
901
 
902
+ # Set execution_partition in variables
903
+ payload['variables']['execution_partition'] = \
904
+ os.sep.join([
905
+ str(pipeline_schedule.id),
906
+ payload['execution_date'].strftime(format='%Y%m%dT%H%M%S_%f'),
907
+ ])
908
+
896
909
  is_integration = PipelineType.INTEGRATION == pipeline_type
897
910
  if is_integration:
898
911
  payload['create_block_runs'] = False
@@ -1115,7 +1128,9 @@ def schedule_all():
1115
1128
  job_manager.clean_up_jobs()
1116
1129
 
1117
1130
 
1118
- def schedule_with_event(event: Dict = dict()):
1131
+ def schedule_with_event(event: Dict = None):
1132
+ if event is None:
1133
+ event = dict()
1119
1134
  logger.info(f'Schedule with event {event}')
1120
1135
  all_event_matchers = EventMatcher.active_event_matchers()
1121
1136
  for e in all_event_matchers:
@@ -1145,7 +1160,9 @@ def sync_schedules(pipeline_uuids: List[str]):
1145
1160
  PipelineSchedule.create_or_update(pipeline_trigger)
1146
1161
 
1147
1162
 
1148
- def get_variables(pipeline_run, extra_variables: Dict = {}) -> Dict:
1163
+ def get_variables(pipeline_run, extra_variables: Dict = None) -> Dict:
1164
+ if extra_variables is None:
1165
+ extra_variables = dict()
1149
1166
  if not pipeline_run:
1150
1167
  return {}
1151
1168
 
@@ -12,4 +12,4 @@ DATAFRAME_OUTPUT_SAMPLE_COUNT = 10
12
12
  # Dockerfile depends on it because it runs ./scripts/install_mage.sh and uses
13
13
  # the last line to determine the version to install.
14
14
  VERSION = \
15
- '0.8.78'
15
+ '0.8.80'