mage-ai 0.8.97__py3-none-any.whl → 0.8.99__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 (152) hide show
  1. mage_ai/api/policies/AutocompleteItemPolicy.py +2 -1
  2. mage_ai/api/policies/BasePolicy.py +2 -2
  3. mage_ai/api/policies/BlockTemplatePolicy.py +2 -1
  4. mage_ai/api/policies/ClusterPolicy.py +2 -1
  5. mage_ai/api/policies/DataProviderPolicy.py +2 -1
  6. mage_ai/api/policies/EventRulePolicy.py +2 -1
  7. mage_ai/api/policies/ExtensionOptionPolicy.py +2 -1
  8. mage_ai/api/policies/FileVersionPolicy.py +2 -1
  9. mage_ai/api/policies/GitBranchPolicy.py +9 -0
  10. mage_ai/api/policies/KernelPolicy.py +2 -1
  11. mage_ai/api/policies/LogPolicy.py +2 -2
  12. mage_ai/api/policies/OauthPolicy.py +15 -0
  13. mage_ai/api/policies/OutputPolicy.py +2 -2
  14. mage_ai/api/policies/PipelinePolicy.py +2 -2
  15. mage_ai/api/policies/PipelineRunPolicy.py +2 -2
  16. mage_ai/api/policies/PipelineSchedulePolicy.py +2 -2
  17. mage_ai/api/policies/PullRequestPolicy.py +64 -0
  18. mage_ai/api/policies/SessionPolicy.py +4 -1
  19. mage_ai/api/policies/VariablePolicy.py +2 -2
  20. mage_ai/api/policies/WidgetPolicy.py +2 -2
  21. mage_ai/api/policies/WorkspacePolicy.py +3 -3
  22. mage_ai/api/presenters/PipelinePresenter.py +1 -0
  23. mage_ai/api/presenters/PullRequestPresenter.py +16 -0
  24. mage_ai/api/presenters/StatusPresenter.py +2 -0
  25. mage_ai/api/presenters/SyncPresenter.py +1 -0
  26. mage_ai/api/presenters/WorkspacePresenter.py +2 -0
  27. mage_ai/api/resources/GitBranchResource.py +81 -26
  28. mage_ai/api/resources/OauthResource.py +31 -4
  29. mage_ai/api/resources/PipelineResource.py +8 -1
  30. mage_ai/api/resources/PullRequestResource.py +87 -0
  31. mage_ai/api/resources/RoleResource.py +6 -3
  32. mage_ai/api/resources/SecretResource.py +2 -5
  33. mage_ai/api/resources/SessionResource.py +18 -0
  34. mage_ai/api/resources/StatusResource.py +7 -3
  35. mage_ai/api/resources/UserResource.py +11 -16
  36. mage_ai/api/resources/WorkspaceResource.py +83 -53
  37. mage_ai/authentication/oauth/active_directory.py +17 -0
  38. mage_ai/authentication/oauth/constants.py +9 -0
  39. mage_ai/authentication/oauth/utils.py +2 -1
  40. mage_ai/authentication/oauth2.py +9 -3
  41. mage_ai/cli/main.py +94 -51
  42. mage_ai/cluster_manager/kubernetes/workload_manager.py +141 -45
  43. mage_ai/data_preparation/git/__init__.py +86 -16
  44. mage_ai/data_preparation/git/api.py +175 -0
  45. mage_ai/data_preparation/models/block/dbt/utils/__init__.py +49 -14
  46. mage_ai/data_preparation/models/block/sql/__init__.py +3 -2
  47. mage_ai/data_preparation/models/pipeline.py +4 -1
  48. mage_ai/data_preparation/models/pipelines/integration_pipeline.py +7 -3
  49. mage_ai/data_preparation/preferences.py +4 -2
  50. mage_ai/data_preparation/repo_manager.py +41 -10
  51. mage_ai/data_preparation/shared/secrets.py +5 -6
  52. mage_ai/data_preparation/sync/__init__.py +2 -1
  53. mage_ai/data_preparation/sync/git_sync.py +2 -5
  54. mage_ai/data_preparation/templates/utils.py +2 -0
  55. mage_ai/orchestration/db/models/oauth.py +22 -4
  56. mage_ai/orchestration/pipeline_scheduler.py +19 -8
  57. mage_ai/orchestration/queue/process_queue.py +15 -12
  58. mage_ai/server/api/clusters.py +21 -11
  59. mage_ai/server/constants.py +1 -1
  60. mage_ai/server/frontend_dist/404.html +2 -2
  61. mage_ai/server/frontend_dist/404.html.html +2 -2
  62. mage_ai/server/frontend_dist/_next/static/WRxCTOtmZhTqQws_7OJZD/_buildManifest.js +1 -0
  63. mage_ai/server/frontend_dist/_next/static/chunks/{1286-993725c925c56a98.js → 1286-b90bd4b7f8abfc3a.js} +1 -1
  64. mage_ai/server/frontend_dist/_next/static/chunks/{1424-f475cae42f8a7fca.js → 1424-90c0f66ba2f86b88.js} +1 -1
  65. mage_ai/server/frontend_dist/_next/static/chunks/3883-c95563b9f60ae526.js +1 -0
  66. mage_ai/server/frontend_dist/_next/static/chunks/6694-c8f2a68074420906.js +1 -0
  67. mage_ai/server/frontend_dist/_next/static/chunks/{9350-1ff50f1d7b9ee754.js → 9350-5191c83a8d0cf454.js} +1 -1
  68. mage_ai/server/frontend_dist/_next/static/chunks/pages/{_app-3527178abd99bc87.js → _app-171846e16d26855a.js} +1 -1
  69. mage_ai/server/frontend_dist/_next/static/chunks/pages/files-e4e778f8f5e1bf2e.js +1 -0
  70. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/settings-c788c1b127999825.js +1 -0
  71. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/[user]-b4650224a19e8fe6.js +1 -0
  72. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/new-931eb719e3fae29c.js +1 -0
  73. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-d3724bde0b186dd9.js +1 -0
  74. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-af11f9cf94024ac0.js +1 -0
  75. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/{[...slug]-3ec5eb9562e4bff4.js → [...slug]-34326db259f922d1.js} +1 -1
  76. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-503ecb7a72257b79.js +1 -0
  77. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/{[run]-7667080098731e30.js → [run]-2994b8ab7862c07b.js} +1 -1
  78. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs-7b31b851e2544b42.js +1 -0
  79. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/{[...slug]-e18058e13882b20d.js → [...slug]-4445619d4eabe065.js} +1 -1
  80. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-6854c10d5589d394.js → triggers-b7db0b682fadb840.js} +1 -1
  81. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/account/profile-ee0931af3abb55b3.js +1 -0
  82. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-f8a59d718751be9a.js +1 -0
  83. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/sync-data-90f8830890036eb2.js +1 -0
  84. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-9f82673fc438ea83.js +1 -0
  85. mage_ai/server/frontend_dist/_next/static/chunks/pages/sign-in-a1871b8a537d823c.js +1 -0
  86. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-48859b4e9c846212.js +1 -0
  87. mage_ai/server/frontend_dist/files.html +2 -2
  88. mage_ai/server/frontend_dist/index.html +2 -2
  89. mage_ai/server/frontend_dist/manage/settings.html +24 -0
  90. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  91. mage_ai/server/frontend_dist/manage/users/new.html +24 -0
  92. mage_ai/server/frontend_dist/manage/users.html +2 -2
  93. mage_ai/server/frontend_dist/manage.html +2 -2
  94. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  95. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  96. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  97. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  98. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  99. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  100. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  101. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  102. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  103. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  104. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  105. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  106. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  107. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  108. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  109. mage_ai/server/frontend_dist/pipelines.html +2 -2
  110. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  111. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  112. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  113. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  114. mage_ai/server/frontend_dist/settings.html +2 -2
  115. mage_ai/server/frontend_dist/sign-in.html +2 -2
  116. mage_ai/server/frontend_dist/terminal.html +2 -2
  117. mage_ai/server/frontend_dist/test.html +2 -2
  118. mage_ai/server/frontend_dist/triggers.html +2 -2
  119. mage_ai/server/frontend_dist/version-control.html +2 -2
  120. mage_ai/server/scheduler_manager.py +7 -2
  121. mage_ai/server/server.py +37 -3
  122. mage_ai/server/terminal_server.py +2 -2
  123. mage_ai/server/websocket_server.py +6 -2
  124. mage_ai/services/newrelic/__init__.py +21 -0
  125. mage_ai/settings/__init__.py +32 -0
  126. mage_ai/shared/hash.py +2 -0
  127. mage_ai/tests/api/test_utils.py +29 -2
  128. mage_ai/tests/data_preparation/models/test_pipeline.py +5 -0
  129. {mage_ai-0.8.97.dist-info → mage_ai-0.8.99.dist-info}/METADATA +8 -3
  130. {mage_ai-0.8.97.dist-info → mage_ai-0.8.99.dist-info}/RECORD +136 -127
  131. mage_ai/data_preparation/templates/main/projects/__init__.py +0 -0
  132. mage_ai/server/frontend_dist/_next/static/YLZRSrQ0aqtl-GGePfsMB/_buildManifest.js +0 -1
  133. mage_ai/server/frontend_dist/_next/static/chunks/3077-d58f18ed770e5137.js +0 -1
  134. mage_ai/server/frontend_dist/_next/static/chunks/3714-b676173cd4d8d86c.js +0 -1
  135. mage_ai/server/frontend_dist/_next/static/chunks/pages/files-82b5409dac9564f4.js +0 -1
  136. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/[user]-bb6aaa23e92a5add.js +0 -1
  137. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-c91ee702a4cd7a6f.js +0 -1
  138. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage-7961010cb0fb9abd.js +0 -1
  139. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-7b8ce89f0d717465.js +0 -1
  140. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs-5bd17a8f3f3d57ef.js +0 -1
  141. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/account/profile-7d75e42d5f4936bb.js +0 -1
  142. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-8220c1200472bf70.js +0 -1
  143. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/sync-data-b602fa9b6ffabd12.js +0 -1
  144. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-3f9d5800f268a263.js +0 -1
  145. mage_ai/server/frontend_dist/_next/static/chunks/pages/sign-in-2925c2c1b0c5559a.js +0 -1
  146. mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-5ffc663cfb0ec81e.js +0 -1
  147. /mage_ai/server/frontend_dist/_next/static/{YLZRSrQ0aqtl-GGePfsMB → WRxCTOtmZhTqQws_7OJZD}/_middlewareManifest.js +0 -0
  148. /mage_ai/server/frontend_dist/_next/static/{YLZRSrQ0aqtl-GGePfsMB → WRxCTOtmZhTqQws_7OJZD}/_ssgManifest.js +0 -0
  149. {mage_ai-0.8.97.dist-info → mage_ai-0.8.99.dist-info}/LICENSE +0 -0
  150. {mage_ai-0.8.97.dist-info → mage_ai-0.8.99.dist-info}/WHEEL +0 -0
  151. {mage_ai-0.8.97.dist-info → mage_ai-0.8.99.dist-info}/entry_points.txt +0 -0
  152. {mage_ai-0.8.97.dist-info → mage_ai-0.8.99.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,175 @@
1
+ import asyncio
2
+ import requests
3
+ import subprocess
4
+ from datetime import datetime
5
+ from git.remote import RemoteProgress
6
+ from git.repo.base import Repo
7
+ from mage_ai.authentication.oauth.constants import OAUTH_PROVIDER_GITHUB
8
+ from mage_ai.orchestration.db.models.oauth import Oauth2AccessToken, Oauth2Application, User
9
+ from typing import Dict
10
+ from urllib.parse import urlsplit, urlunsplit
11
+
12
+
13
+ API_ENDPOINT = 'https://api.github.com'
14
+
15
+
16
+ def get_access_token_for_user(user: User) -> Oauth2AccessToken:
17
+ oauth_client = Oauth2Application.query.filter(
18
+ Oauth2Application.client_id == OAUTH_PROVIDER_GITHUB,
19
+ ).first()
20
+
21
+ if oauth_client:
22
+ access_token = Oauth2AccessToken.query.filter(
23
+ Oauth2AccessToken.expires > datetime.utcnow(),
24
+ Oauth2AccessToken.oauth2_application_id == oauth_client.id,
25
+ ).first()
26
+
27
+ return access_token
28
+
29
+
30
+ def fetch(remote_name: str, remote_url: str, token: str) -> RemoteProgress:
31
+ from mage_ai.data_preparation.git import Git
32
+
33
+ custom_progress = RemoteProgress()
34
+ username = get_username(token)
35
+
36
+ url = build_authenticated_remote_url(remote_url, username, token)
37
+ git_manager = Git.get_manager()
38
+
39
+ remote = git_manager.repo.remotes[remote_name]
40
+ url_original = list(remote.urls)[0]
41
+ remote.set_url(url)
42
+
43
+ try:
44
+ remote.fetch(progress=custom_progress)
45
+ except Exception as err:
46
+ raise err
47
+ finally:
48
+ try:
49
+ remote.set_url(url_original)
50
+ except Exception as err:
51
+ print('WARNING (mage_ai.data_preparation.git.api):')
52
+ print(err)
53
+
54
+ return custom_progress
55
+
56
+
57
+ def pull(remote_name: str, remote_url: str, branch_name: str, token: str) -> RemoteProgress:
58
+ from mage_ai.data_preparation.git import Git
59
+
60
+ custom_progress = RemoteProgress()
61
+ username = get_username(token)
62
+
63
+ url = build_authenticated_remote_url(remote_url, username, token)
64
+ git_manager = Git.get_manager()
65
+
66
+ remote = git_manager.repo.remotes[remote_name]
67
+ url_original = list(remote.urls)[0]
68
+ remote.set_url(url)
69
+
70
+ try:
71
+ remote.pull(branch_name, custom_progress)
72
+ except Exception as err:
73
+ raise err
74
+ finally:
75
+ try:
76
+ remote.set_url(url_original)
77
+ except Exception as err:
78
+ print('WARNING (mage_ai.data_preparation.git.api):')
79
+ print(err)
80
+
81
+ return custom_progress
82
+
83
+
84
+ def push(remote_name: str, remote_url: str, branch_name: str, token: str) -> RemoteProgress:
85
+ from mage_ai.data_preparation.git import Git
86
+
87
+ custom_progress = RemoteProgress()
88
+ username = get_username(token)
89
+
90
+ url = build_authenticated_remote_url(remote_url, username, token)
91
+ git_manager = Git.get_manager()
92
+
93
+ remote = git_manager.repo.remotes[remote_name]
94
+ url_original = list(remote.urls)[0]
95
+ remote.set_url(url)
96
+
97
+ try:
98
+ remote.push(branch_name, custom_progress)
99
+ except Exception as err:
100
+ raise err
101
+ finally:
102
+ try:
103
+ remote.set_url(url_original)
104
+ except Exception as err:
105
+ print('WARNING (mage_ai.data_preparation.git.api):')
106
+ print(err)
107
+
108
+ return custom_progress
109
+
110
+
111
+ def build_authenticated_remote_url(remote_url: str, username: str, token: str) -> str:
112
+ # https://[username]:[token]@github.com/[remote_url]
113
+ url = urlsplit(remote_url)
114
+ url = url._replace(netloc=f'{username}:{token}@{url.netloc}')
115
+ return urlunsplit(url)
116
+
117
+
118
+ def get_username(token: str) -> str:
119
+ return get_user(token)['login']
120
+
121
+
122
+ def get_user(token: str) -> Dict:
123
+ """
124
+ https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
125
+ """
126
+ resp = requests.get(f'{API_ENDPOINT}/user', headers={
127
+ 'Accept': 'application/vnd.github+json',
128
+ 'Authorization': f'Bearer {token}',
129
+ 'X-GitHub-Api-Version': '2022-11-28',
130
+ })
131
+
132
+ return resp.json()
133
+
134
+
135
+ def check_connection(repo: Repo, remote_url: str) -> None:
136
+ asyncio.run(validate_authentication_for_remote_url(repo, remote_url))
137
+
138
+
139
+ async def validate_authentication_for_remote_url(repo: Repo, remote_url: str) -> None:
140
+ proc = repo.git.ls_remote(remote_url, as_process=True)
141
+
142
+ asyncio.run(__poll_process_with_timeout(
143
+ proc,
144
+ error_message='Error connecting to remote, make sure your access is valid.',
145
+ ))
146
+
147
+
148
+ async def __poll_process_with_timeout(
149
+ proc: subprocess.Popen,
150
+ error_message: str = None,
151
+ timeout: int = 10,
152
+ ):
153
+ ct = 0
154
+ while ct < timeout * 2:
155
+ return_code = proc.poll()
156
+ if return_code is not None:
157
+ proc.kill()
158
+ break
159
+ ct += 1
160
+ await asyncio.sleep(0.5)
161
+
162
+ if error_message is None:
163
+ error_message = 'Error running Git process'
164
+
165
+ if return_code is not None and return_code != 0:
166
+ _, err = proc.communicate()
167
+ message = (
168
+ err.decode('UTF-8') if err
169
+ else error_message
170
+ )
171
+ raise ChildProcessError(message)
172
+
173
+ if return_code is None:
174
+ proc.kill()
175
+ raise TimeoutError(error_message)
@@ -1,11 +1,26 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import uuid
1
7
  from contextlib import redirect_stdout
2
8
  from datetime import datetime
3
- from jinja2 import Template
4
9
  from logging import Logger
10
+ from typing import Callable, Dict, List, Tuple
11
+
12
+ import aiofiles
13
+ import simplejson
14
+ import yaml
15
+ from jinja2 import Template
16
+ from pandas import DataFrame
17
+
5
18
  from mage_ai.data_preparation.models.block import Block
19
+ from mage_ai.data_preparation.models.block.sql import bigquery
6
20
  from mage_ai.data_preparation.models.block.sql import (
7
- bigquery,
8
21
  execute_sql_code as execute_sql_code_orig,
22
+ )
23
+ from mage_ai.data_preparation.models.block.sql import (
9
24
  mssql,
10
25
  mysql,
11
26
  postgres,
@@ -13,6 +28,7 @@ from mage_ai.data_preparation.models.block.sql import (
13
28
  snowflake,
14
29
  spark,
15
30
  trino,
31
+ clickhouse,
16
32
  )
17
33
  from mage_ai.data_preparation.models.constants import BlockLanguage, BlockType
18
34
  from mage_ai.data_preparation.repo_manager import get_repo_path
@@ -27,18 +43,6 @@ from mage_ai.shared.hash import merge_dict
27
43
  from mage_ai.shared.parsers import encode_complex
28
44
  from mage_ai.shared.strings import remove_extension_from_filename
29
45
  from mage_ai.shared.utils import clean_name, files_in_path
30
- from pandas import DataFrame
31
- from typing import Callable, Dict, List, Tuple
32
- import aiofiles
33
- import os
34
- import re
35
- import shutil
36
- import simplejson
37
- import subprocess
38
- import sys
39
- import uuid
40
- import yaml
41
-
42
46
 
43
47
  PROFILES_FILE_NAME = 'profiles.yml'
44
48
 
@@ -594,6 +598,23 @@ def config_file_loader_and_configuration(block, profile_target: str) -> Dict:
594
598
  data_provider_schema=schema,
595
599
  export_write_policy=ExportWritePolicy.REPLACE,
596
600
  )
601
+ elif DataSource.CLICKHOUSE == profile_type:
602
+ database = profile.get('schema')
603
+ interface = profile.get('driver')
604
+
605
+ config_file_loader = ConfigFileLoader(config=dict(
606
+ CLICKHOUSE_DATABASE=database,
607
+ CLICKHOUSE_HOST=profile.get('host'),
608
+ CLICKHOUSE_INTERFACE=interface,
609
+ CLICKHOUSE_PASSWORD=profile.get('password'),
610
+ CLICKHOUSE_PORT=profile.get('port'),
611
+ CLICKHOUSE_USERNAME=profile.get('user'),
612
+ ))
613
+ configuration = dict(
614
+ data_provider=profile_type,
615
+ data_provider_database=database,
616
+ export_write_policy=ExportWritePolicy.REPLACE,
617
+ )
597
618
 
598
619
  if not config_file_loader or not configuration:
599
620
  attr = parse_attributes(block)
@@ -730,6 +751,15 @@ def create_upstream_tables(
730
751
  block,
731
752
  **kwargs_shared,
732
753
  )
754
+ elif DataSource.CLICKHOUSE == data_provider:
755
+ from mage_ai.io.clickhouse import ClickHouse
756
+
757
+ loader = ClickHouse.with_config(config_file_loader)
758
+ clickhouse.create_upstream_block_tables(
759
+ loader,
760
+ block,
761
+ **kwargs_shared,
762
+ )
733
763
 
734
764
  block.upstream_blocks = upstream_blocks_init
735
765
 
@@ -927,6 +957,11 @@ def execute_query(
927
957
 
928
958
  with Trino.with_config(config_file_loader) as loader:
929
959
  return loader.load(query_string, **shared_kwargs)
960
+ elif DataSource.CLICKHOUSE == data_provider:
961
+ from mage_ai.io.clickhouse import ClickHouse
962
+
963
+ loader = ClickHouse.with_config(config_file_loader)
964
+ return loader.load(query_string, **shared_kwargs)
930
965
 
931
966
 
932
967
  def query_from_compiled_sql(block, profile_target: str, limit: int = None) -> DataFrame:
@@ -137,8 +137,9 @@ def execute_sql_code(
137
137
  NotFound: 404 Not found: Table database:schema.table_name
138
138
  was not found in location XX
139
139
  """
140
+ total_retries = 5
140
141
  tries = 0
141
- while tries < 10:
142
+ while tries < total_retries:
142
143
  sleep(tries)
143
144
  tries += 1
144
145
  try:
@@ -149,7 +150,7 @@ def execute_sql_code(
149
150
  )
150
151
  return [result]
151
152
  except Exception as err:
152
- if '404' not in str(err):
153
+ if '404' not in str(err) or tries == total_retries:
153
154
  raise err
154
155
  elif DataSource.CLICKHOUSE.value == data_provider:
155
156
  from mage_ai.io.clickhouse import ClickHouse
@@ -53,6 +53,7 @@ class Pipeline:
53
53
  self.executor_type = None
54
54
  self.executor_config = dict()
55
55
  self.name = None
56
+ self.notification_config = dict()
56
57
  self.repo_path = repo_path or get_repo_path()
57
58
  self.schedules = []
58
59
  self.uuid = uuid
@@ -425,7 +426,8 @@ class Pipeline:
425
426
  self.callback_configs = config.get('callbacks') or []
426
427
  self.conditional_configs = config.get('conditionals') or []
427
428
  self.executor_type = config.get('executor_type')
428
- self.executor_config = config.get('executor_confid') or dict()
429
+ self.executor_config = config.get('executor_config') or dict()
430
+ self.notification_config = config.get('notification_config') or dict()
429
431
  self.spark_config = config.get('spark_config') or dict()
430
432
  self.widget_configs = config.get('widgets') or []
431
433
 
@@ -543,6 +545,7 @@ class Pipeline:
543
545
  executor_count=self.executor_count,
544
546
  executor_type=self.executor_type,
545
547
  name=self.name,
548
+ notification_config=self.notification_config,
546
549
  type=self.type.value if type(self.type) is not str else self.type,
547
550
  updated_at=self.updated_at,
548
551
  uuid=self.uuid,
@@ -212,6 +212,8 @@ class IntegrationPipeline(Pipeline):
212
212
  error = dig(json_object, 'tags.error')
213
213
  except Exception:
214
214
  error = line
215
+ elif not error and line.startswith('CRITICAL'):
216
+ error = line
215
217
  raise Exception(error)
216
218
 
217
219
  def preview_data(self, block_type: BlockType, streams: List[str] = None) -> List[str]:
@@ -286,9 +288,11 @@ class IntegrationPipeline(Pipeline):
286
288
  except Exception:
287
289
  error = line
288
290
  if not error:
289
- raise Exception('The sample data was not able to be loaded. Please check \
290
- if the stream still exists. If it does not, click the "View and \
291
- select streams" button and confirm the valid streams.')
291
+ raise Exception('The sample data was not able to be loaded. Please check if the ' +
292
+ 'stream still exists. If it does not, click the "View and select ' +
293
+ 'streams" button and confirm the valid streams. If it does, ' +
294
+ 'loading sample data for this source may not currently ' +
295
+ 'be supported.')
292
296
  raise Exception(error)
293
297
 
294
298
  def count_records(self) -> List[Dict]:
@@ -16,7 +16,8 @@ GIT_USERNAME_VAR = 'GIT_USERNAME'
16
16
  GIT_EMAIL_VAR = 'GIT_EMAIL'
17
17
  GIT_AUTH_TYPE_VAR = 'GIT_AUTH_TYPE'
18
18
  GIT_BRANCH_VAR = 'GIT_BRANCH'
19
- GIT_SYNC_ON_PIPELINE_RUN_TYPE = 'GIT_SYNC_ON_PIPELINE_RUN'
19
+ GIT_SYNC_ON_PIPELINE_RUN_VAR = 'GIT_SYNC_ON_PIPELINE_RUN'
20
+ GIT_SYNC_ON_START_VAR = 'GIT_SYNC_ON_PIPELINE_RUN'
20
21
 
21
22
 
22
23
  class Preferences:
@@ -52,7 +53,8 @@ class Preferences:
52
53
  username=os.getenv(GIT_USERNAME_VAR),
53
54
  email=os.getenv(GIT_EMAIL_VAR),
54
55
  branch=os.getenv(GIT_BRANCH_VAR),
55
- sync_on_pipeline_run=bool(int(os.getenv(GIT_SYNC_ON_PIPELINE_RUN_TYPE) or 0)),
56
+ sync_on_pipeline_run=bool(int(os.getenv(GIT_SYNC_ON_PIPELINE_RUN_VAR) or 0)),
57
+ sync_on_start=bool(int(os.getenv(GIT_SYNC_ON_START_VAR) or 0)),
56
58
  )
57
59
  else:
58
60
  project_sync_config = project_preferences.get('sync_config', dict())
@@ -1,7 +1,7 @@
1
1
  import os
2
- import shutil
3
2
  import sys
4
3
  import traceback
4
+ import uuid
5
5
  from enum import Enum
6
6
  from typing import Dict
7
7
 
@@ -16,7 +16,6 @@ from mage_ai.data_preparation.shared.constants import (
16
16
  from mage_ai.data_preparation.templates.utils import copy_template_directory
17
17
  from mage_ai.shared.environments import is_test
18
18
 
19
-
20
19
  if is_test():
21
20
  DEFAULT_MAGE_DATA_DIR = './'
22
21
  else:
@@ -155,28 +154,34 @@ class RepoConfig:
155
154
  yml.dump(data, f)
156
155
 
157
156
 
158
- def init_repo(repo_path: str, project_type: str = ProjectType.STANDALONE) -> None:
157
+ def init_repo(
158
+ repo_path: str,
159
+ project_type: str = ProjectType.STANDALONE,
160
+ cluster_type: str = None,
161
+ project_uuid: str = None,
162
+ ) -> None:
159
163
  """
160
164
  Initialize a repository under the current path.
161
165
  """
162
166
  if os.path.exists(repo_path):
163
167
  raise FileExistsError(f'Repository {repo_path} already exists')
164
168
 
169
+ new_config = dict()
165
170
  if project_type == ProjectType.MAIN:
166
171
  copy_template_directory('main', repo_path)
172
+ new_config.update(
173
+ cluster_type=cluster_type,
174
+ )
167
175
  elif project_type == ProjectType.SUB:
168
176
  os.makedirs(
169
177
  os.getenv(MAGE_DATA_DIR_ENV_VAR) or DEFAULT_MAGE_DATA_DIR,
170
178
  exist_ok=True,
171
179
  )
172
180
  copy_template_directory('repo', repo_path)
173
- new_repo_config = get_repo_config(repo_path)
174
- current_metadata = get_repo_config().metadata_path
175
- new_metadata = new_repo_config.metadata_path
176
- if os.path.exists(current_metadata):
177
- shutil.copyfile(current_metadata, new_metadata)
178
- new_repo_config.save(
181
+ new_config.update(
179
182
  project_type=ProjectType.SUB.value,
183
+ cluster_type=cluster_type,
184
+ project_uuid=project_uuid,
180
185
  )
181
186
  else:
182
187
  os.makedirs(
@@ -185,6 +190,11 @@ def init_repo(repo_path: str, project_type: str = ProjectType.STANDALONE) -> Non
185
190
  )
186
191
  copy_template_directory('repo', repo_path)
187
192
 
193
+ if not project_uuid:
194
+ project_uuid = uuid.uuid4().hex
195
+ new_config.update(project_uuid=project_uuid)
196
+ get_repo_config(repo_path).save(**new_config)
197
+
188
198
 
189
199
  def get_data_dir() -> str:
190
200
  return os.getenv(MAGE_DATA_DIR_ENV_VAR) or DEFAULT_MAGE_DATA_DIR
@@ -203,7 +213,11 @@ def get_repo_config(repo_path=None) -> RepoConfig:
203
213
 
204
214
 
205
215
  def get_project_type(repo_path=None) -> ProjectType:
206
- return get_repo_config(repo_path=repo_path).project_type
216
+ try:
217
+ return get_repo_config(repo_path=repo_path).project_type
218
+ except Exception:
219
+ # default to standalone project type
220
+ return ProjectType.STANDALONE
207
221
 
208
222
 
209
223
  def set_repo_path(repo_path: str) -> None:
@@ -213,3 +227,20 @@ def set_repo_path(repo_path: str) -> None:
213
227
 
214
228
  def get_variables_dir(repo_path: str = None) -> str:
215
229
  return get_repo_config(repo_path=repo_path).variables_dir
230
+
231
+
232
+ config = get_repo_config()
233
+ project_uuid = config.project_uuid
234
+
235
+
236
+ def update_project_uuid():
237
+ global project_uuid
238
+ project_uuid = get_repo_config().project_uuid
239
+ if not project_uuid:
240
+ puuid = uuid.uuid4().hex
241
+ config.save(project_uuid=puuid)
242
+ project_uuid = puuid
243
+
244
+
245
+ def get_project_uuid() -> str:
246
+ return project_uuid
@@ -1,10 +1,9 @@
1
- from cryptography.fernet import Fernet, InvalidToken
2
- from mage_ai.data_preparation.repo_manager import (
3
- get_data_dir,
4
- get_repo_path
5
- )
6
- from typing import List
7
1
  import os
2
+ from typing import List
3
+
4
+ from cryptography.fernet import Fernet, InvalidToken
5
+
6
+ from mage_ai.data_preparation.repo_manager import get_data_dir, get_repo_path
8
7
 
9
8
  DEFAULT_MAGE_SECRETS_DIR = 'secrets'
10
9
 
@@ -17,10 +17,11 @@ class AuthType(str, Enum):
17
17
 
18
18
  @dataclass
19
19
  class GitConfig(BaseConfig):
20
- remote_repo_link: str
20
+ remote_repo_link: str = None
21
21
  repo_path: str = os.getcwd()
22
22
  branch: str = 'main'
23
23
  sync_on_pipeline_run: bool = False
24
+ sync_on_start: bool = False
24
25
  auth_type: AuthType = AuthType.SSH
25
26
  # User settings moved to UserGitConfig, these will be used for Git syncs
26
27
  username: str = ''
@@ -11,12 +11,9 @@ class GitSync(BaseSync):
11
11
  self.git_manager = Git(sync_config)
12
12
 
13
13
  def sync_data(self):
14
- with VerboseFunctionExec(
15
- f'Syncing data with remote repo {self.remote_repo_link}',
16
- verbose=True,
17
- ):
18
- self.git_manager.reset(self.branch)
14
+ self.git_manager.reset(self.branch)
19
15
 
16
+ # Reset git sync by cloning the remote repo
20
17
  def reset(self):
21
18
  with VerboseFunctionExec(
22
19
  f'Attempting to clone from remote repo {self.remote_repo_link}',
@@ -51,6 +51,8 @@ def write_template(template_source: str, dest_path: str) -> None:
51
51
  template_source (str): Template source code to write to file
52
52
  dest_path (str): Destination file to write template source code to.
53
53
  """
54
+ os.makedirs(os.path.dirname(dest_path), exist_ok=True)
55
+
54
56
  with open(dest_path, 'w') as foutput:
55
57
  foutput.write(template_source)
56
58
 
@@ -15,7 +15,7 @@ from sqlalchemy import (
15
15
  )
16
16
  from sqlalchemy.orm import relationship, validates
17
17
 
18
- from mage_ai.data_preparation.repo_manager import get_repo_path
18
+ from mage_ai.data_preparation.repo_manager import get_project_uuid, get_repo_path
19
19
  from mage_ai.orchestration.db import db_connection, safe_db_query
20
20
  from mage_ai.orchestration.db.errors import ValidationError
21
21
  from mage_ai.orchestration.db.models.base import BaseModel
@@ -70,7 +70,7 @@ class User(BaseModel):
70
70
 
71
71
  @property
72
72
  def project_access(self) -> int:
73
- return self.get_access(Permission.Entity.PROJECT, get_repo_path())
73
+ return self.get_access(Permission.Entity.PROJECT, get_project_uuid())
74
74
 
75
75
  def get_access(
76
76
  self,
@@ -195,11 +195,13 @@ class Role(BaseModel):
195
195
 
196
196
  def get_access(
197
197
  self,
198
- entity: Union['Permission.Entity', None] = None,
198
+ entity: Union['Permission.Entity', None],
199
199
  entity_id: Union[str, None] = None,
200
200
  ) -> int:
201
201
  permissions = []
202
202
  if entity is None:
203
+ return 0
204
+ elif entity == Permission.Entity.ANY:
203
205
  permissions.extend(self.permissions)
204
206
  else:
205
207
  entity_permissions = list(filter(
@@ -225,7 +227,7 @@ class Role(BaseModel):
225
227
  we will go up the entity chain to see if there are permissions for parent entities.
226
228
  '''
227
229
  if entity == Permission.Entity.PIPELINE:
228
- return self.get_access(Permission.Entity.PROJECT, get_repo_path())
230
+ return self.get_access(Permission.Entity.PROJECT, get_project_uuid())
229
231
  elif entity == Permission.Entity.PROJECT:
230
232
  return self.get_access(Permission.Entity.GLOBAL)
231
233
  else:
@@ -239,6 +241,9 @@ class UserRole(BaseModel):
239
241
 
240
242
  class Permission(BaseModel):
241
243
  class Entity(str, enum.Enum):
244
+ # Permissions saved to the DB should not have the "ANY" entity. It should only be used
245
+ # when evaluating permissions.
246
+ ANY = 'any'
242
247
  GLOBAL = 'global'
243
248
  PROJECT = 'project'
244
249
  PIPELINE = 'pipeline'
@@ -260,6 +265,19 @@ class Permission(BaseModel):
260
265
 
261
266
  role = relationship(Role, back_populates='permissions')
262
267
 
268
+ @validates('entity')
269
+ def validate_entity(self, key, value):
270
+ if value == Permission.Entity.ANY:
271
+ raise ValidationError(
272
+ 'Permission entity cannot be ANY. Please select a specific entity.',
273
+ metadata=dict(
274
+ key=key,
275
+ value=value,
276
+ ),
277
+ )
278
+
279
+ return value
280
+
263
281
  @classmethod
264
282
  @safe_db_query
265
283
  def create_default_permissions(
@@ -76,7 +76,12 @@ class PipelineScheduler:
76
76
  )
77
77
  self.logger = DictLogger(self.logger_manager.logger)
78
78
  self.notification_sender = NotificationSender(
79
- NotificationConfig.load(config=self.pipeline.repo_config.notification_config),
79
+ NotificationConfig.load(
80
+ config=merge_dict(
81
+ self.pipeline.repo_config.notification_config,
82
+ self.pipeline.notification_config,
83
+ )
84
+ )
80
85
  )
81
86
 
82
87
  self.allow_blocks_to_fail = (
@@ -85,12 +90,13 @@ class PipelineScheduler:
85
90
  )
86
91
 
87
92
  def start(self, should_schedule: bool = True) -> None:
88
- if get_preferences().sync_config:
93
+ preferences = get_preferences()
94
+ if preferences.sync_config:
89
95
  tags = dict(
90
96
  pipeline_run_id=self.pipeline_run.id,
91
97
  pipeline_uuid=self.pipeline.uuid,
92
98
  )
93
- sync_config = GitConfig.load(config=get_preferences().sync_config)
99
+ sync_config = GitConfig.load(config=preferences.sync_config)
94
100
  if sync_config.sync_on_pipeline_run:
95
101
  sync = GitSync(sync_config)
96
102
  try:
@@ -1060,10 +1066,6 @@ def check_sla():
1060
1066
  pipeline_runs = PipelineRun.in_progress_runs(pipeline_schedules)
1061
1067
 
1062
1068
  if pipeline_runs:
1063
- notification_sender = NotificationSender(
1064
- NotificationConfig.load(config=get_repo_config(get_repo_path()).notification_config),
1065
- )
1066
-
1067
1069
  current_time = datetime.now(tz=pytz.UTC)
1068
1070
  # TODO: combine all SLA alerts in one notification
1069
1071
  for pipeline_run in pipeline_runs:
@@ -1076,8 +1078,17 @@ def check_sla():
1076
1078
  else pipeline_run.created_at
1077
1079
  if compare(start_date, current_time - timedelta(seconds=sla)) == 1:
1078
1080
  # passed SLA for pipeline_run
1081
+ pipeline = Pipeline.get(pipeline_run.pipeline_schedule.pipeline_uuid)
1082
+ notification_sender = NotificationSender(
1083
+ NotificationConfig.load(
1084
+ config=merge_dict(
1085
+ pipeline.repo_config.notification_config,
1086
+ pipeline.notification_config,
1087
+ ),
1088
+ ),
1089
+ )
1079
1090
  notification_sender.send_pipeline_run_sla_passed_message(
1080
- Pipeline.get(pipeline_run.pipeline_schedule.pipeline_uuid),
1091
+ pipeline,
1081
1092
  pipeline_run,
1082
1093
  )
1083
1094