Skip to content

Commit 041a624

Browse files
[td] Add models for custom design framework (mage-ai#4192)
1 parent c916bb8 commit 041a624

File tree

10 files changed

+203
-0
lines changed

10 files changed

+203
-0
lines changed
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from mage_ai.api.oauth_scope import OauthScope
2+
from mage_ai.api.operations.constants import OperationType
3+
from mage_ai.api.policies.BasePolicy import BasePolicy
4+
from mage_ai.api.presenters.CustomDesignPresenter import CustomDesignPresenter
5+
6+
7+
class CustomDesignPolicy(BasePolicy):
8+
pass
9+
10+
11+
CustomDesignPolicy.allow_actions(
12+
[
13+
OperationType.LIST,
14+
],
15+
scopes=[
16+
OauthScope.CLIENT_PRIVATE,
17+
],
18+
condition=lambda policy: policy.has_at_least_viewer_role(),
19+
override_permission_condition=lambda _policy: True,
20+
)
21+
22+
23+
CustomDesignPolicy.allow_read(
24+
CustomDesignPresenter.default_attributes + [],
25+
scopes=[
26+
OauthScope.CLIENT_PRIVATE,
27+
],
28+
on_action=[
29+
OperationType.LIST,
30+
],
31+
condition=lambda policy: policy.has_at_least_viewer_role(),
32+
override_permission_condition=lambda _policy: True,
33+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Dict
2+
3+
from mage_ai.api.presenters.BasePresenter import BasePresenter
4+
5+
6+
class CustomDesignPresenter(BasePresenter):
7+
default_attributes = [
8+
'components',
9+
'pages',
10+
'project',
11+
]
12+
13+
async def prepare_present(self, **kwargs) -> Dict:
14+
return self.resource.model.to_dict()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Dict
2+
3+
from mage_ai.api.resources.AsyncBaseResource import AsyncBaseResource
4+
from mage_ai.presenters.design.models import CustomDesign
5+
6+
7+
class CustomDesignResource(AsyncBaseResource):
8+
@classmethod
9+
async def collection(self, query: Dict, _meta: Dict, user, **kwargs):
10+
custom_design = CustomDesign.load_from_file(all_configurations=True)
11+
12+
arr = [custom_design]
13+
14+
if custom_design.custom_designs:
15+
arr.extend(list(custom_design.custom_designs.values()))
16+
17+
return self.build_result_set(arr, user, **kwargs)

mage_ai/authentication/permissions/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class EntityName(str, Enum):
2121
ComputeConnection = 'ComputeConnection'
2222
ComputeService = 'ComputeService'
2323
ConfigurationOption = 'ConfigurationOption'
24+
CustomDesign = 'CustomDesign'
2425
CustomTemplate = 'CustomTemplate'
2526
DataProvider = 'DataProvider'
2627
Database = 'Database'

mage_ai/data_preparation/models/project/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class FeatureUUID(str, Enum):
55
ADD_NEW_BLOCK_V2 = 'add_new_block_v2'
66
COMPUTE_MANAGEMENT = 'compute_management'
77
DATA_INTEGRATION_IN_BATCH_PIPELINE = 'data_integration_in_batch_pipeline'
8+
DBT_V2 = 'dbt_v2'
89
GLOBAL_HOOKS = 'global_hooks'
910
INTERACTIONS = 'interactions'
1011
LOCAL_TIMEZONE = 'display_local_timezone'

mage_ai/data_preparation/models/project/models.py

+10
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ class ProjectPipelines(BaseDataClass):
1010

1111
def __post_init__(self):
1212
self.serialize_attribute_class('settings', PipelineSettings)
13+
14+
15+
@dataclass
16+
class ProjectDataClass(BaseDataClass):
17+
full_path: str = None
18+
full_path_relative: str = None
19+
path: str = None
20+
root_project_name: str = None
21+
root_project_full_path: str = None
22+
uuid: str = None

mage_ai/frontend/interfaces/ProjectType.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export enum FeatureUUIDEnum {
44
ADD_NEW_BLOCK_V2 = 'add_new_block_v2',
55
COMPUTE_MANAGEMENT = 'compute_management',
66
DATA_INTEGRATION_IN_BATCH_PIPELINE = 'data_integration_in_batch_pipeline',
7+
DBT_V2 = 'dbt_v2',
78
GLOBAL_HOOKS = 'global_hooks',
89
INTERACTIONS = 'interactions',
910
NOTEBOOK_BLOCK_OUTPUT_SPLIT_VIEW = 'notebook_block_output_split_view',

mage_ai/presenters/design/__init__.py

Whitespace-only changes.
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CUSTOM_DESIGN_FILENAME = 'design.yaml'

mage_ai/presenters/design/models.py

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import os
2+
from dataclasses import dataclass
3+
from typing import Dict
4+
5+
import yaml
6+
7+
from mage_ai.data_preparation.models.project.models import ProjectDataClass
8+
from mage_ai.presenters.design.constants import CUSTOM_DESIGN_FILENAME
9+
from mage_ai.settings.platform import (
10+
build_repo_path_for_all_projects,
11+
project_platform_activated,
12+
)
13+
from mage_ai.settings.repo import get_repo_path
14+
from mage_ai.shared.io import safe_write
15+
from mage_ai.shared.models import BaseDataClass
16+
17+
18+
@dataclass
19+
class DesignComponentConfigurations(BaseDataClass):
20+
logo: Dict = None
21+
22+
23+
@dataclass
24+
class DesignComponents(BaseDataClass):
25+
header: DesignComponentConfigurations = None
26+
27+
def __post_init__(self):
28+
self.serialize_attribute_class('header', DesignComponentConfigurations)
29+
30+
31+
@dataclass
32+
class DesignPageConfigurations(BaseDataClass):
33+
edit: Dict = None
34+
35+
36+
@dataclass
37+
class DesignPages(BaseDataClass):
38+
pipelines: DesignPageConfigurations = None
39+
40+
def __post_init__(self):
41+
self.serialize_attribute_class('pipelines', DesignPageConfigurations)
42+
43+
44+
@dataclass
45+
class CustomDesign(BaseDataClass):
46+
components: DesignComponents = None
47+
custom_designs: Dict = None
48+
pages: DesignPages = None
49+
project: ProjectDataClass = None
50+
51+
def __post_init__(self):
52+
self.serialize_attribute_class('components', DesignComponents)
53+
self.serialize_attribute_class('pages', DesignPages)
54+
self.serialize_attribute_class('project', ProjectDataClass)
55+
56+
@classmethod
57+
def file_path(self, repo_path: str = None) -> str:
58+
return os.path.join(repo_path or get_repo_path(), CUSTOM_DESIGN_FILENAME)
59+
60+
@classmethod
61+
def load_from_file(
62+
self,
63+
all_configurations: bool = True,
64+
file_path: str = None,
65+
repo_path: str = None,
66+
project: Dict = None,
67+
) -> 'CustomDesign':
68+
model = self.__load_from_file(file_path=file_path, project=project, repo_path=repo_path)
69+
70+
if all_configurations and project_platform_activated():
71+
model.custom_designs = {}
72+
73+
for project_name, project in build_repo_path_for_all_projects().items():
74+
full_path = project['full_path']
75+
76+
model.custom_designs[project_name] = self.__load_from_file(
77+
file_path=os.path.join(full_path, CUSTOM_DESIGN_FILENAME),
78+
project=project,
79+
)
80+
81+
return model
82+
83+
@classmethod
84+
def __load_from_file(
85+
self,
86+
file_path: str = None,
87+
project: Dict = None,
88+
repo_path: str = None,
89+
) -> 'CustomDesign':
90+
yaml_config = {}
91+
92+
file_path_to_use = file_path or self.file_path(repo_path=repo_path)
93+
if os.path.exists(file_path_to_use):
94+
with open(file_path_to_use, 'r') as fp:
95+
content = fp.read()
96+
if content:
97+
yaml_config = yaml.safe_load(content) or {}
98+
99+
return self.load(project=project, **yaml_config)
100+
101+
def save(self, file_path: str = None, repo_path: str = None) -> None:
102+
if not file_path:
103+
file_path = self.file_path(repo_path=repo_path)
104+
105+
content_original = None
106+
if os.path.exists(file_path):
107+
with open(file_path) as f:
108+
content_original = f.read()
109+
110+
with open(file_path, 'w'):
111+
try:
112+
data = self.to_dict()
113+
content = yaml.safe_dump(data)
114+
safe_write(file_path, content)
115+
except Exception as err:
116+
if content_original:
117+
safe_write(file_path, content_original)
118+
raise err
119+
120+
def to_dict(self, **kwargs) -> Dict:
121+
return super().to_dict(
122+
convert_enum=True,
123+
ignore_attributes=['custom_designs'],
124+
ignore_empty=True,
125+
)

0 commit comments

Comments
 (0)