mage-ai 0.8.47__py3-none-any.whl → 0.8.49__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 (101) hide show
  1. mage_ai/api/resources/DataProviderResource.py +13 -1
  2. mage_ai/data_preparation/git/__init__.py +1 -1
  3. mage_ai/data_preparation/models/block/__init__.py +5 -0
  4. mage_ai/data_preparation/models/block/sql/__init__.py +68 -8
  5. mage_ai/data_preparation/models/block/sql/clickhouse.py +38 -0
  6. mage_ai/data_preparation/models/block/utils.py +4 -2
  7. mage_ai/data_preparation/models/file.py +4 -3
  8. mage_ai/data_preparation/models/pipeline.py +36 -3
  9. mage_ai/data_preparation/templates/repo/io_config.yaml +12 -0
  10. mage_ai/io/base.py +1 -0
  11. mage_ai/io/clickhouse.py +237 -0
  12. mage_ai/io/config.py +19 -0
  13. mage_ai/io/io_config.py +1 -0
  14. mage_ai/io/snowflake.py +0 -1
  15. mage_ai/io/sql.py +4 -1
  16. mage_ai/server/constants.py +1 -1
  17. mage_ai/server/frontend_dist/404.html +2 -2
  18. mage_ai/server/frontend_dist/404.html.html +2 -2
  19. mage_ai/server/frontend_dist/_next/static/I2bRLK9B7Lap2LvCicTKv/_buildManifest.js +1 -0
  20. mage_ai/server/frontend_dist/_next/static/chunks/{2249-50d2156ae3ce5933.js → 2249-59feb0a0585ef7c1.js} +1 -1
  21. mage_ai/server/frontend_dist/_next/static/chunks/2407-35c362852abff411.js +1 -0
  22. mage_ai/server/frontend_dist/_next/static/chunks/2524-ecbe3dd70d06cbe4.js +1 -0
  23. mage_ai/server/frontend_dist/_next/static/chunks/{3014-26ba07eb6a78e31c.js → 3014-a01e16bc067500ad.js} +1 -1
  24. mage_ai/server/frontend_dist/_next/static/chunks/{4506-ce5fa63b65f6fa5f.js → 4506-a3c53b0033972626.js} +1 -1
  25. mage_ai/server/frontend_dist/_next/static/chunks/4741-5fbecd08232ba3f9.js +1 -0
  26. mage_ai/server/frontend_dist/_next/static/chunks/{5477-793cd2120261d023.js → 5477-b439f211b6146a11.js} +1 -1
  27. mage_ai/server/frontend_dist/_next/static/chunks/5716-c5952e5ca9c8e139.js +1 -0
  28. mage_ai/server/frontend_dist/_next/static/chunks/{5872-103815a4a043489b.js → 5872-1767c45ee6690ae5.js} +1 -1
  29. mage_ai/server/frontend_dist/_next/static/chunks/{5896-f84e336fb8877027.js → 5896-10a676bcc86978cc.js} +1 -1
  30. mage_ai/server/frontend_dist/_next/static/chunks/{6166-5fc2768ef661e7da.js → 6166-97cb3cc7d00d645c.js} +1 -1
  31. mage_ai/server/frontend_dist/_next/static/chunks/{6532-b0fd357eb359c127.js → 6532-5f86e9e1f32adf3a.js} +1 -1
  32. mage_ai/server/frontend_dist/_next/static/chunks/{7400-acab00d6265bd4dc.js → 7400-033fb12e7c46f62e.js} +1 -1
  33. mage_ai/server/frontend_dist/_next/static/chunks/{9832-c8b8970bb522f302.js → 9832-f97919376d52e3bf.js} +1 -1
  34. mage_ai/server/frontend_dist/_next/static/chunks/pages/{manage-96c89b2a2689f80c.js → manage-8a7b2b707c7038fb.js} +1 -1
  35. mage_ai/server/frontend_dist/_next/static/chunks/pages/{pipeline-runs-294eccc0163c80a1.js → pipeline-runs-832218136947bf56.js} +1 -1
  36. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-c48a45cea6df6192.js +1 -0
  37. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-dd637dcb042ff771.js +1 -0
  38. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-5541c3e13297ddbc.js +1 -0
  39. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{logs-0e6935a2b7f8e1ed.js → logs-caa9fade2bc783b9.js} +1 -1
  40. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/{block-runs-0e02bcdfcd533830.js → block-runs-1b44f28966826dbe.js} +1 -1
  41. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/{block-runtime-baf8bb6da01976db.js → block-runtime-44029312dc580504.js} +1 -1
  42. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{monitors-d42158676a75b451.js → monitors-9b46466e066d6372.js} +1 -1
  43. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/{[run]-4049561456ea2314.js → [run]-5f78885afc7758df.js} +1 -1
  44. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{runs-bebeee2854c734fc.js → runs-43f2c1658cd3d03d.js} +1 -1
  45. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{settings-b95afd173fb265df.js → settings-8fb53eb8dd0267e1.js} +1 -1
  46. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{syncs-e46a9b03e0fff91a.js → syncs-c92351b1a81f7a13.js} +1 -1
  47. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/{[...slug]-6a95567766a1d00e.js → [...slug]-ca76f5938bdff8a7.js} +1 -1
  48. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-c259bd9d385404fa.js → triggers-4e9518882c8730b1.js} +1 -1
  49. mage_ai/server/frontend_dist/_next/static/chunks/pages/{pipelines-0900be641ce4e6ec.js → pipelines-c46347f0308ab09a.js} +1 -1
  50. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{preferences-25735fdf80f95015.js → preferences-45c40fb24c8a5139.js} +1 -1
  51. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{sync-data-abfaa2b15eef72a0.js → sync-data-6e80afa0c9b90cdf.js} +1 -1
  52. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{users-5d0c51213b9bba8e.js → users-12a90f932ff4fc9d.js} +1 -1
  53. mage_ai/server/frontend_dist/_next/static/chunks/pages/{terminal-24eb5df948cc9a7e.js → terminal-cc54b4d4e5295101.js} +1 -1
  54. mage_ai/server/frontend_dist/_next/static/chunks/pages/test-bd801bde63db7c9e.js +1 -0
  55. mage_ai/server/frontend_dist/_next/static/chunks/pages/{triggers-6ff85ea842bde8c2.js → triggers-08774e473ea4a96b.js} +1 -1
  56. mage_ai/server/frontend_dist/index.html +2 -2
  57. mage_ai/server/frontend_dist/manage.html +2 -2
  58. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  59. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  60. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  61. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  62. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  63. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  64. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  65. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  66. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  67. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  68. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  69. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  70. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  71. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  72. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  73. mage_ai/server/frontend_dist/pipelines.html +2 -2
  74. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  75. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  76. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  77. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  78. mage_ai/server/frontend_dist/settings.html +2 -2
  79. mage_ai/server/frontend_dist/sign-in.html +2 -2
  80. mage_ai/server/frontend_dist/terminal.html +2 -2
  81. mage_ai/server/frontend_dist/test.html +3 -3
  82. mage_ai/server/frontend_dist/triggers.html +2 -2
  83. mage_ai/server/utils/output_display.py +1 -3
  84. mage_ai/server/websocket_server.py +0 -1
  85. {mage_ai-0.8.47.dist-info → mage_ai-0.8.49.dist-info}/METADATA +4 -1
  86. {mage_ai-0.8.47.dist-info → mage_ai-0.8.49.dist-info}/RECORD +92 -90
  87. mage_ai/server/frontend_dist/_next/static/chunks/2083-78a438dbdc57d1e4.js +0 -1
  88. mage_ai/server/frontend_dist/_next/static/chunks/2524-6aeb9419acf5d1b4.js +0 -1
  89. mage_ai/server/frontend_dist/_next/static/chunks/4538-8a3c3e47be976ede.js +0 -1
  90. mage_ai/server/frontend_dist/_next/static/chunks/4741-acfe40cf3b06659e.js +0 -1
  91. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-e512333242e02c60.js +0 -1
  92. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-774dd41941497640.js +0 -1
  93. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-c6179a9e737009c9.js +0 -1
  94. mage_ai/server/frontend_dist/_next/static/chunks/pages/test-85cf18ae78ff535c.js +0 -1
  95. mage_ai/server/frontend_dist/_next/static/xTScbs8HLliQCDHzVPx5E/_buildManifest.js +0 -1
  96. /mage_ai/server/frontend_dist/_next/static/{xTScbs8HLliQCDHzVPx5E → I2bRLK9B7Lap2LvCicTKv}/_middlewareManifest.js +0 -0
  97. /mage_ai/server/frontend_dist/_next/static/{xTScbs8HLliQCDHzVPx5E → I2bRLK9B7Lap2LvCicTKv}/_ssgManifest.js +0 -0
  98. {mage_ai-0.8.47.dist-info → mage_ai-0.8.49.dist-info}/LICENSE +0 -0
  99. {mage_ai-0.8.47.dist-info → mage_ai-0.8.49.dist-info}/WHEEL +0 -0
  100. {mage_ai-0.8.47.dist-info → mage_ai-0.8.49.dist-info}/entry_points.txt +0 -0
  101. {mage_ai-0.8.47.dist-info → mage_ai-0.8.49.dist-info}/top_level.txt +0 -0
@@ -8,12 +8,24 @@ import yaml
8
8
 
9
9
  DATA_PROVIDERS = [
10
10
  DataSource.BIGQUERY,
11
+ DataSource.CLICKHOUSE,
11
12
  DataSource.MSSQL,
12
13
  DataSource.MYSQL,
13
14
  DataSource.POSTGRES,
14
15
  DataSource.REDSHIFT,
15
16
  DataSource.SNOWFLAKE,
17
+ DataSource.TRINO,
16
18
  ]
19
+ DATA_PROVIDERS_NAME = {
20
+ DataSource.BIGQUERY: 'BigQuery',
21
+ DataSource.CLICKHOUSE: 'ClickHouse',
22
+ DataSource.MSSQL: 'Microsoft SQL Server',
23
+ DataSource.MYSQL: 'MySQL',
24
+ DataSource.POSTGRES: 'PostgreSQL',
25
+ DataSource.REDSHIFT: 'Redshift',
26
+ DataSource.SNOWFLAKE: 'Snowflake',
27
+ DataSource.TRINO: 'Trino',
28
+ }
17
29
 
18
30
 
19
31
  class DataProviderResource(GenericResource):
@@ -27,7 +39,7 @@ class DataProviderResource(GenericResource):
27
39
  print(exc)
28
40
 
29
41
  collection = [dict(
30
- id=ds.title(),
42
+ id=DATA_PROVIDERS_NAME[ds.value],
31
43
  profiles=[p for p in profiles if p != 'version'],
32
44
  value=ds.value,
33
45
  ) for ds in DATA_PROVIDERS]
@@ -5,7 +5,6 @@ from mage_ai.orchestration.db.models.oauth import User
5
5
  from urllib.parse import urlparse
6
6
  import asyncio
7
7
  import base64
8
- import git
9
8
  import os
10
9
  import subprocess
11
10
 
@@ -15,6 +14,7 @@ REMOTE_NAME = 'mage-repo'
15
14
 
16
15
  class Git:
17
16
  def __init__(self, git_config: GitConfig):
17
+ import git
18
18
  self.remote_repo_link = git_config.remote_repo_link
19
19
  self.repo_path = git_config.repo_path or os.getcwd()
20
20
  os.makedirs(self.repo_path, exist_ok=True)
@@ -655,6 +655,7 @@ class Block:
655
655
  dynamic_block_uuid: str = None,
656
656
  dynamic_upstream_block_uuids: List[str] = None,
657
657
  run_settings: Dict = None,
658
+ **kwargs,
658
659
  ) -> Dict:
659
660
  try:
660
661
  if not run_all_blocks:
@@ -685,6 +686,7 @@ class Block:
685
686
  dynamic_block_index=dynamic_block_index,
686
687
  dynamic_upstream_block_uuids=dynamic_upstream_block_uuids,
687
688
  run_settings=run_settings,
689
+ **kwargs,
688
690
  )
689
691
  block_output = self.post_process_output(output)
690
692
  variable_mapping = dict()
@@ -867,6 +869,7 @@ class Block:
867
869
  dynamic_block_index: int = None,
868
870
  dynamic_upstream_block_uuids: List[str] = None,
869
871
  run_settings: Dict = None,
872
+ **kwargs,
870
873
  ) -> Dict:
871
874
  # Add pipeline uuid and block uuid to global_vars
872
875
  global_vars = merge_dict(
@@ -922,6 +925,7 @@ class Block:
922
925
  runtime_arguments=runtime_arguments,
923
926
  upstream_block_uuids=upstream_block_uuids,
924
927
  run_settings=run_settings,
928
+ **kwargs,
925
929
  )
926
930
 
927
931
  output_message = dict(output=outputs)
@@ -942,6 +946,7 @@ class Block:
942
946
  runtime_arguments: Dict = None,
943
947
  upstream_block_uuids: List[str] = None,
944
948
  run_settings: Dict = None,
949
+ **kwargs,
945
950
  ) -> List:
946
951
  decorated_functions = []
947
952
  test_functions = []
@@ -1,6 +1,7 @@
1
1
  from mage_ai.data_preparation.models.block import Block
2
2
  from mage_ai.data_preparation.models.block.sql import (
3
3
  bigquery,
4
+ clickhouse,
4
5
  mssql,
5
6
  mysql,
6
7
  postgres,
@@ -14,8 +15,12 @@ from mage_ai.data_preparation.models.block.sql.utils.shared import (
14
15
  )
15
16
  from mage_ai.data_preparation.models.constants import BlockType
16
17
  from mage_ai.data_preparation.repo_manager import get_repo_path
17
- from mage_ai.io.base import DataSource, ExportWritePolicy
18
- from mage_ai.io.config import ConfigFileLoader
18
+ from mage_ai.io.base import (
19
+ DataSource,
20
+ ExportWritePolicy,
21
+ QUERY_ROW_LIMIT,
22
+ )
23
+ from mage_ai.io.config import ConfigFileLoader, ConfigKey
19
24
  from os import path
20
25
  from time import sleep
21
26
  from typing import Any, Dict, List
@@ -37,6 +42,7 @@ def execute_sql_code(
37
42
  global_vars: Dict = None,
38
43
  config_file_loader: Any = None,
39
44
  configuration: Dict = None,
45
+ test_execution: bool = False,
40
46
  ) -> List[Any]:
41
47
  configuration = configuration if configuration else block.configuration
42
48
  use_raw_sql = configuration.get('use_raw_sql')
@@ -66,6 +72,12 @@ def execute_sql_code(
66
72
  verbose=BlockType.DATA_EXPORTER == block.type,
67
73
  )
68
74
 
75
+ limit = int(configuration.get('limit') or QUERY_ROW_LIMIT)
76
+ if test_execution:
77
+ limit = min(limit, QUERY_ROW_LIMIT)
78
+ else:
79
+ limit = QUERY_ROW_LIMIT
80
+
69
81
  if DataSource.BIGQUERY.value == data_provider:
70
82
  from mage_ai.io.bigquery import BigQuery
71
83
 
@@ -111,12 +123,54 @@ def execute_sql_code(
111
123
  try:
112
124
  result = loader.load(
113
125
  f'SELECT * FROM {database}.{schema}.{table_name}',
126
+ limit=limit,
114
127
  verbose=False,
115
128
  )
116
129
  return [result]
117
130
  except Exception as err:
118
131
  if '404' not in str(err):
119
132
  raise err
133
+ elif DataSource.CLICKHOUSE.value == data_provider:
134
+ from mage_ai.io.clickhouse import ClickHouse
135
+
136
+ loader = ClickHouse.with_config(config_file_loader)
137
+ clickhouse.create_upstream_block_tables(
138
+ loader,
139
+ block,
140
+ configuration=configuration,
141
+ execution_partition=execution_partition,
142
+ )
143
+
144
+ query_string = clickhouse.interpolate_input_data(block, query)
145
+ query_string = interpolate_vars(
146
+ query_string, global_vars=global_vars)
147
+
148
+ database = database or 'default'
149
+
150
+ if use_raw_sql:
151
+ return execute_raw_sql(
152
+ loader,
153
+ block,
154
+ query_string,
155
+ configuration=configuration,
156
+ should_query=should_query,
157
+ )
158
+ else:
159
+ loader.export(
160
+ None,
161
+ table_name=table_name,
162
+ database=database,
163
+ query_string=query_string,
164
+ **kwargs_shared,
165
+ )
166
+
167
+ if should_query:
168
+ return [
169
+ loader.load(
170
+ f'SELECT * FROM {database}.{table_name}',
171
+ verbose=False,
172
+ ),
173
+ ]
120
174
  elif DataSource.MSSQL.value == data_provider:
121
175
  from mage_ai.io.mssql import MSSQL
122
176
 
@@ -155,6 +209,7 @@ def execute_sql_code(
155
209
  return [
156
210
  loader.load(
157
211
  f'SELECT * FROM {table_name}',
212
+ limit=limit,
158
213
  verbose=False,
159
214
  ),
160
215
  ]
@@ -193,6 +248,7 @@ def execute_sql_code(
193
248
  return [
194
249
  loader.load(
195
250
  f'SELECT * FROM {table_name}',
251
+ limit=limit,
196
252
  verbose=False,
197
253
  ),
198
254
  ]
@@ -231,6 +287,7 @@ def execute_sql_code(
231
287
  return [
232
288
  loader.load(
233
289
  f'SELECT * FROM {schema}.{table_name}',
290
+ limit=limit,
234
291
  verbose=False,
235
292
  ),
236
293
  ]
@@ -269,6 +326,7 @@ def execute_sql_code(
269
326
  return [
270
327
  loader.load(
271
328
  f'SELECT * FROM {schema}.{table_name}',
329
+ limit=limit,
272
330
  verbose=False,
273
331
  ),
274
332
  ]
@@ -316,6 +374,7 @@ def execute_sql_code(
316
374
  database=database,
317
375
  schema=schema,
318
376
  table_name=table_name,
377
+ limit=limit,
319
378
  verbose=False,
320
379
  ),
321
380
  ]
@@ -344,21 +403,20 @@ def execute_sql_code(
344
403
  else:
345
404
  loader.export(
346
405
  None,
347
- table_name,
348
- database,
349
406
  schema,
407
+ table_name,
350
408
  if_exists=export_write_policy,
351
409
  query_string=query_string,
352
410
  verbose=BlockType.DATA_EXPORTER == block.type,
353
411
  )
354
412
 
355
413
  if should_query:
414
+ catalog = config_file_loader[ConfigKey.TRINO_CATALOG]
415
+
356
416
  return [
357
417
  loader.load(
358
- f'SELECT * FROM "{database}"."{schema}"."{table_name}"',
359
- database=database,
360
- schema=schema,
361
- table_name=table_name,
418
+ f'SELECT * FROM "{catalog}"."{schema}"."{table_name}"',
419
+ limit=limit,
362
420
  verbose=False,
363
421
  ),
364
422
  ]
@@ -449,9 +507,11 @@ class SQLBlock(Block):
449
507
  global_vars=None,
450
508
  **kwargs,
451
509
  ) -> List:
510
+ test_execution = kwargs.get('test_execution')
452
511
  return execute_sql_code(
453
512
  self,
454
513
  custom_code or self.content,
455
514
  execution_partition=execution_partition,
456
515
  global_vars=global_vars,
516
+ test_execution=test_execution,
457
517
  )
@@ -0,0 +1,38 @@
1
+ from mage_ai.data_preparation.models.block.sql.utils.shared import (
2
+ create_upstream_block_tables as create_upstream_block_tables_orig,
3
+ interpolate_input,
4
+ )
5
+ from mage_ai.io.config import ConfigKey
6
+ from typing import Dict
7
+
8
+
9
+ def create_upstream_block_tables(
10
+ loader,
11
+ block,
12
+ cascade_on_drop: bool = False,
13
+ configuration: Dict = None,
14
+ execution_partition: str = None,
15
+ cache_upstream_dbt_models: bool = False,
16
+ ):
17
+ create_upstream_block_tables_orig(
18
+ loader,
19
+ block,
20
+ cascade_on_drop,
21
+ configuration,
22
+ execution_partition,
23
+ cache_upstream_dbt_models,
24
+ cache_keys=[
25
+ ConfigKey.CLICKHOUSE_DATABASE,
26
+ ConfigKey.CLICKHOUSE_HOST,
27
+ ConfigKey.CLICKHOUSE_PORT,
28
+ ],
29
+ no_schema=True,
30
+ )
31
+
32
+
33
+ def interpolate_input_data(block, query):
34
+ return interpolate_input(
35
+ block,
36
+ query,
37
+ lambda db, schema, tn: tn,
38
+ )
@@ -163,8 +163,10 @@ def create_block_runs_from_dynamic_block(
163
163
  skip_creating_downstream = True
164
164
  break
165
165
 
166
- down_uuids = [down.uuid for down in dynamic_ancestor.downstream_blocks]
167
- down_uuids_as_ancestors = [i for i in down_uuids if i in ancestors_uuids]
166
+ down_uuids_as_ancestors = []
167
+ for down in dynamic_ancestor.downstream_blocks:
168
+ if down.uuid in ancestors_uuids and not should_reduce_output(down):
169
+ down_uuids_as_ancestors.append(down.uuid)
168
170
  skip_creating_downstream = len(down_uuids_as_ancestors) >= 2
169
171
 
170
172
  # Only create downstream block runs if it doesn’t have dynamically created upstream
@@ -20,7 +20,7 @@ INACCESSIBLE_DIRS = frozenset(['__pycache__'])
20
20
  MAX_DEPTH = 30
21
21
  MAX_NUMBER_OF_FILE_VERSIONS = int(os.getenv('MAX_NUMBER_OF_FILE_VERSIONS', 100) or 100)
22
22
 
23
- PIPELINES_FOLDER_PREFIX = 'pipelines/'
23
+ PIPELINES_FOLDER_PREFIX = f'pipelines{os.sep}'
24
24
 
25
25
 
26
26
  class File:
@@ -118,8 +118,9 @@ class File:
118
118
  filename: str,
119
119
  ) -> Tuple[str, str]:
120
120
  return os.path.join(
121
- f'{repo_path}/{FILE_VERSIONS_DIR}',
122
- dir_path.replace(repo_path, f'{repo_path}/{FILE_VERSIONS_DIR}'),
121
+ repo_path,
122
+ FILE_VERSIONS_DIR,
123
+ dir_path.replace(repo_path, os.path.join(repo_path, FILE_VERSIONS_DIR)),
123
124
  filename,
124
125
  )
125
126
 
@@ -21,7 +21,7 @@ from mage_ai.data_preparation.templates.utils import copy_template_directory
21
21
  from mage_ai.data_preparation.variable_manager import VariableManager
22
22
  from mage_ai.orchestration.db import db_connection, safe_db_query
23
23
  from mage_ai.shared.array import find
24
- from mage_ai.shared.hash import extract, ignore_keys, merge_dict
24
+ from mage_ai.shared.hash import extract, index_by, ignore_keys, merge_dict
25
25
  from mage_ai.shared.io import safe_write, safe_write_async
26
26
  from mage_ai.shared.strings import format_enum
27
27
  from mage_ai.shared.utils import clean_name
@@ -674,6 +674,12 @@ class Pipeline:
674
674
  )
675
675
  should_save = True
676
676
 
677
+ blocks = data.get('blocks', [])
678
+
679
+ if blocks:
680
+ if not should_save and self.__update_block_order(blocks):
681
+ should_save = True
682
+
677
683
  if should_save:
678
684
  self.save()
679
685
 
@@ -682,8 +688,8 @@ class Pipeline:
682
688
 
683
689
  arr = []
684
690
 
685
- if 'blocks' in data:
686
- arr.append(('blocks', data['blocks'], self.blocks_by_uuid))
691
+ if blocks:
692
+ arr.append(('blocks', blocks, self.blocks_by_uuid))
687
693
  if 'widgets' in data:
688
694
  arr.append(('widgets', data['widgets'], self.widgets_by_uuid))
689
695
 
@@ -778,6 +784,33 @@ class Pipeline:
778
784
  if should_save_async:
779
785
  await self.save_async(widget=widget)
780
786
 
787
+ def __update_block_order(self, blocks: List[Dict]) -> bool:
788
+ uuids_new = [b['uuid'] for b in blocks]
789
+ uuids_old = [b['uuid'] for b in self.block_configs]
790
+
791
+ min_length = min(len(uuids_new), len(uuids_old))
792
+
793
+ # If there are no blocks or the order has changed
794
+ if min_length == 0 or uuids_new[:min_length] == uuids_old[:min_length]:
795
+ return False
796
+
797
+ block_configs_by_uuids = index_by(lambda x: x['uuid'], self.block_configs)
798
+
799
+ block_configs = []
800
+ blocks_by_uuid = {}
801
+
802
+ for block_uuid in uuids_new:
803
+ if block_uuid in block_configs_by_uuids:
804
+ block_configs.append(block_configs_by_uuids[block_uuid])
805
+
806
+ if block_uuid in self.blocks_by_uuid:
807
+ blocks_by_uuid[block_uuid] = self.blocks_by_uuid[block_uuid]
808
+
809
+ self.block_configs = block_configs
810
+ self.blocks_by_uuid = blocks_by_uuid
811
+
812
+ return True
813
+
781
814
  def __add_block_to_mapping(
782
815
  self,
783
816
  blocks_by_uuid,
@@ -10,6 +10,12 @@ default:
10
10
  AZURE_CLIENT_SECRET: "{{ env_var('AZURE_CLIENT_SECRET') }}"
11
11
  AZURE_STORAGE_ACCOUNT_NAME: "{{ env_var('AZURE_STORAGE_ACCOUNT_NAME') }}"
12
12
  AZURE_TENANT_ID: "{{ env_var('AZURE_TENANT_ID') }}"
13
+ CLICKHOUSE_DATABASE: default
14
+ CLICKHOUSE_HOST: host.docker.internal
15
+ CLICKHOUSE_INTERFACE: http
16
+ CLICKHOUSE_PASSWORD: null
17
+ CLICKHOUSE_PORT: 8123
18
+ CLICKHOUSE_USERNAME: null
13
19
  GOOGLE_SERVICE_ACC_KEY:
14
20
  type: service_account
15
21
  project_id: project-id
@@ -46,3 +52,9 @@ default:
46
52
  SNOWFLAKE_DEFAULT_DB: optional_default_database
47
53
  SNOWFLAKE_DEFAULT_SCHEMA: optional_default_schema
48
54
  SNOWFLAKE_ROLE: role
55
+ TRINO_CATALOG: catalog
56
+ TRINO_HOST: '127.0.0.1'
57
+ TRINO_PASSWORD: pasword
58
+ TRINO_PORT: 8080
59
+ TRINO_SCHEMA: schema
60
+ TRINO_USER: user
mage_ai/io/base.py CHANGED
@@ -14,6 +14,7 @@ QUERY_ROW_LIMIT = 10_000_000
14
14
  class DataSource(str, Enum):
15
15
  API = 'api'
16
16
  BIGQUERY = 'bigquery'
17
+ CLICKHOUSE = 'clickhouse'
17
18
  FILE = 'file'
18
19
  GOOGLE_CLOUD_STORAGE = 'google_cloud_storage'
19
20
  KAFKA = 'kafka'
@@ -0,0 +1,237 @@
1
+ from mage_ai.io.base import BaseSQLDatabase, ExportWritePolicy, QUERY_ROW_LIMIT
2
+ from mage_ai.io.config import BaseConfigLoader, ConfigKey
3
+ from mage_ai.io.export_utils import (
4
+ clean_df_for_export,
5
+ infer_dtypes,
6
+ )
7
+ from pandas import DataFrame
8
+ from typing import Dict, List, Union
9
+ import clickhouse_connect
10
+
11
+
12
+ class ClickHouse(BaseSQLDatabase):
13
+ """
14
+ Handles data transfer betwee a ClickHouse data warehouse and the Mage app.
15
+ """
16
+ def __init__(self, **kwargs) -> None:
17
+ """
18
+ Initializes settings for connecting to a ClickHouse warehouse.
19
+
20
+ To authenticate (and authorize) access to a ClickHouse warehouse,
21
+ credentials, i.e., username and password, must be provided.
22
+
23
+ All keyword arguments will be passed to the ClickHouse client.
24
+ """
25
+ if kwargs.get('verbose') is not None:
26
+ kwargs.pop('verbose')
27
+ super().__init__(verbose=kwargs.get('verbose', True))
28
+ with self.printer.print_msg('Connecting to ClickHouse'):
29
+ self.client = clickhouse_connect.get_client(**kwargs)
30
+
31
+ @classmethod
32
+ def with_config(cls, config: BaseConfigLoader) -> 'ClickHouse':
33
+ """
34
+ Initializes ClickHouse client from configuration loader
35
+
36
+ Args:
37
+ config (BaseConfigLoader): Configuration loader object
38
+ """
39
+ if ConfigKey.CLICKHOUSE_HOST not in config:
40
+ raise ValueError(
41
+ 'No valid configuration settings found for ClickHouse. '
42
+ 'You must specify host.'
43
+ )
44
+ return cls(
45
+ database=config[ConfigKey.CLICKHOUSE_DATABASE],
46
+ host=config[ConfigKey.CLICKHOUSE_HOST],
47
+ interface=config[ConfigKey.CLICKHOUSE_INTERFACE],
48
+ password=config[ConfigKey.CLICKHOUSE_PASSWORD],
49
+ port=config[ConfigKey.CLICKHOUSE_PORT],
50
+ username=config[ConfigKey.CLICKHOUSE_USERNAME],
51
+ )
52
+
53
+ def execute(self, command_string: str, **kwargs) -> None:
54
+ """
55
+ Sends command to the connected ClickHouse warehouse.
56
+
57
+ Args:
58
+ command_string (str): Command to execute on the ClickHouse warehouse.
59
+ **kwargs: Additional arguments to pass to command, such as configurations
60
+ """
61
+ with self.printer.print_msg(f'Executing query \'{command_string}\''):
62
+ command_string = self._clean_query(command_string)
63
+ self.client.command(command_string, **kwargs)
64
+
65
+ def execute_query(
66
+ self,
67
+ query: str,
68
+ parameters: Dict = None,
69
+ **kwargs,
70
+ ) -> DataFrame:
71
+ """
72
+ Sends query to the connected ClickHouse warehouse.
73
+
74
+ Args:
75
+ query (str): Query to execute on the ClickHouse warehouse.
76
+ **kwargs: Additional arguments to pass to query, such as query configurations
77
+ """
78
+ query = self._clean_query(query)
79
+ with self.printer.print_msg(f'Executing query \'{query}\''):
80
+ result = self.client.query_df(query, parameters=parameters)
81
+
82
+ return result
83
+
84
+ def execute_queries(
85
+ self,
86
+ queries: List[str],
87
+ query_variables: List[Dict] = None,
88
+ fetch_query_at_indexes: List[bool] = None,
89
+ **kwargs,
90
+ ) -> List:
91
+ results = []
92
+
93
+ for idx, query in enumerate(queries):
94
+ parameters = query_variables[idx] \
95
+ if query_variables and idx < len(query_variables) \
96
+ else {}
97
+ query = self._clean_query(query)
98
+
99
+ if fetch_query_at_indexes and idx < len(fetch_query_at_indexes) and \
100
+ fetch_query_at_indexes[idx]:
101
+ result = self.client.query_df(query, parameters=parameters)
102
+ else:
103
+ result = self.client.command(query, parameters=parameters)
104
+
105
+ results.append(result)
106
+
107
+ return results
108
+
109
+ def load(
110
+ self,
111
+ query_string: str,
112
+ limit: int = QUERY_ROW_LIMIT,
113
+ display_query: Union[str, None] = None,
114
+ verbose: bool = True,
115
+ **kwargs,
116
+ ) -> DataFrame:
117
+ """
118
+ Loads data from ClickHouse into a Pandas data frame based on the query given.
119
+ This will fail if the query returns no data from the database. When a select query
120
+ is provided, this function will load at maximum 10,000,000 rows of data. To operate on more
121
+ data, consider performing data transformations in warehouse.
122
+
123
+ Args:
124
+ query_string (str): Query to fetch a table or subset of a table.
125
+ limit (int, Optional): The number of rows to limit the loaded dataframe to. Defaults to
126
+ 10,000,000.
127
+ **kwargs: Additional arguments to pass to query, such as query configurations
128
+
129
+ Returns:
130
+ DataFrame: Data frame associated with the given query.
131
+ """
132
+
133
+ print_message = 'Loading data'
134
+ if verbose:
135
+ print_message += ' with query'
136
+
137
+ if display_query:
138
+ for line in display_query.split('\n'):
139
+ print_message += f'\n{line}'
140
+ else:
141
+ print_message += f'\n{query_string}'
142
+
143
+ query_string = self._clean_query(query_string)
144
+
145
+ with self.printer.print_msg(print_message):
146
+ return self.client.query_df(
147
+ self._enforce_limit(query_string, limit), **kwargs
148
+ )
149
+
150
+ def export(
151
+ self,
152
+ df: DataFrame,
153
+ table_name: str,
154
+ database: str = 'default',
155
+ if_exists: str = 'append',
156
+ index: bool = False,
157
+ query_string: Union[str, None] = None,
158
+ create_table_statement: Union[str, None] = None,
159
+ verbose: bool = True,
160
+ **kwargs,
161
+ ) -> None:
162
+ """
163
+ Exports a Pandas data frame to a ClickHouse warehouse based on the table name.
164
+ If table doesn't exist, the table is automatically created.
165
+
166
+ Args:
167
+ df (DataFrame): Data frame to export
168
+ table_name (str): Name of the table to export data to (excluding database).
169
+ If this table exists, the table schema must match the data frame schema.
170
+ If this table doesn't exist, query_string must be specified to create the new table.
171
+ database (str): Name of the database in which the table is located.
172
+ if_exists (str, optional): Specifies export policy if table exists. Either
173
+ - `'fail'`: throw an error.
174
+ - `'replace'`: drops existing table and creates new table of same name.
175
+ - `'append'`: appends data frame to existing table. In this case the schema must
176
+ match the original table.
177
+ Defaults to `'append'`.
178
+ **kwargs: Additional arguments to pass to writer
179
+ """
180
+
181
+ if type(df) is dict:
182
+ df = DataFrame([df])
183
+ elif type(df) is list:
184
+ df = DataFrame(df)
185
+
186
+ if not query_string:
187
+ if index:
188
+ df = df.reset_index()
189
+
190
+ dtypes = infer_dtypes(df)
191
+ df = clean_df_for_export(df, self.clean, dtypes)
192
+
193
+ def __process(database: Union[str, None]):
194
+
195
+ df_existing = self.client.query_df(f"""
196
+ EXISTS TABLE {database}.{table_name}
197
+ """)
198
+
199
+ table_exists = not df_existing.empty and df_existing.iloc[0, 0] == 1
200
+ should_create_table = not table_exists
201
+
202
+ if table_exists:
203
+ if ExportWritePolicy.FAIL == if_exists:
204
+ raise ValueError(
205
+ f'Table \'{table_name}\' already'
206
+ ' exists in database {database}.',
207
+ )
208
+ elif ExportWritePolicy.REPLACE == if_exists:
209
+ self.client.command(
210
+ f'DROP TABLE IF EXISTS {database}.{table_name}')
211
+ should_create_table = True
212
+
213
+ if query_string:
214
+ self.client.command(f'USE {database}')
215
+
216
+ if should_create_table:
217
+ self.client.command(f"""
218
+ CREATE TABLE IF NOT EXISTS {database}.{table_name} ENGINE = Memory AS
219
+ {query_string}
220
+ """)
221
+ else:
222
+ self.client.command(f"""
223
+ INSERT INTO {database}.{table_name}
224
+ {query_string}
225
+ """)
226
+ else:
227
+ if should_create_table:
228
+ self.client.command(create_table_statement)
229
+
230
+ self.client.insert_df(f'{database}.{table_name}', df)
231
+
232
+ if verbose:
233
+ with self.printer.print_msg(
234
+ f'Exporting data to table \'{database}.{table_name}\''):
235
+ __process(database=database)
236
+ else:
237
+ __process(database=database)