From 201d66b1d5128e235fd0af4c67a99c3c9bc6fddf Mon Sep 17 00:00:00 2001 From: dlimeng Date: Sun, 24 Dec 2023 17:10:30 +0800 Subject: [PATCH] solidui datasource job --- solidui/daos/datasource.py | 10 +- solidui/daos/datasource_type.py | 2 +- solidui/daos/job_element.py | 25 ++ solidui/daos/job_element_page.py | 25 ++ solidui/daos/job_page.py | 38 +++ .../mysql/__init__.py => daos/model_type.py} | 15 +- solidui/datasource_plugin/base.py | 113 ++++++++- solidui/views/job/api.py | 56 +++++ solidui/views/job_page/api.py | 49 +++- solidui/views/metadata/api.py | 219 +++++++++++++++++- solidui/views/model/api.py | 41 +++- solidui/views/model/schemas.py | 25 ++ 12 files changed, 608 insertions(+), 10 deletions(-) create mode 100644 solidui/daos/job_element.py create mode 100644 solidui/daos/job_element_page.py create mode 100644 solidui/daos/job_page.py rename solidui/{datasource_plugin/mysql/__init__.py => daos/model_type.py} (70%) create mode 100644 solidui/views/model/schemas.py diff --git a/solidui/daos/datasource.py b/solidui/daos/datasource.py index dc3a9448..bb3085fb 100644 --- a/solidui/daos/datasource.py +++ b/solidui/daos/datasource.py @@ -14,6 +14,8 @@ from typing import Any import logging +from solidui.extensions import db + from solidui.utils.page_info import PageInfo from solidui.daos.base import BaseDAO @@ -35,7 +37,7 @@ def create_data_source(cls, data_source: DataSource) -> DataSource: return super().create(item=data_source) @classmethod - def get_data_source_name(cls, data_source_name: int) -> DataSource: + def get_data_source_name(cls, data_source_name: str) -> DataSource: return super().find_one_or_none(datasource_name=data_source_name) @classmethod @@ -57,7 +59,7 @@ def exist_data_source(cls, data_source_id: int) -> DataSource: @classmethod def get_data_source_page(cls, page_no: int, page_size: int, name_filter: str = None, type_filter: int = None, expire_filter: bool = None) -> PageInfo: - query = cls.model_cls.query + query = db.session.query(DataSource) # Apply filters if name_filter is not None: @@ -81,7 +83,7 @@ def delete_data_source(cls, data_source_id: int) -> None: if data_source_id is None: raise DAONotFound(message="DataSource id is required") - data_source = cls.find_by_id(data_source_id) + data_source = super().find_by_id(data_source_id) if not data_source: raise DAONotFound(message="DataSource is required") @@ -96,7 +98,7 @@ def update_data_source(cls, data_source: DataSource) -> DataSource: if data_source.id is None: raise DAONotFound(message="DataSource id is required") - existing_data_source = cls.find_by_id(data_source.id) + existing_data_source = super().find_by_id(data_source.id) if not existing_data_source: raise DAONotFound(message="DataSource is required") diff --git a/solidui/daos/datasource_type.py b/solidui/daos/datasource_type.py index a8a1d9f2..2d9c51f0 100644 --- a/solidui/daos/datasource_type.py +++ b/solidui/daos/datasource_type.py @@ -21,7 +21,7 @@ class DataSourceTypeDAO(BaseDAO[DataSourceType]): model_cls = DataSourceType @classmethod - def all_list(cls) -> DataSourceType: + def all_list(cls) -> list[DataSourceType]: return super().find_all() @classmethod diff --git a/solidui/daos/job_element.py b/solidui/daos/job_element.py new file mode 100644 index 00000000..2d4720ef --- /dev/null +++ b/solidui/daos/job_element.py @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Optional +from solidui.daos.base import BaseDAO +from solidui.daos.exceptions import DAONotFound +from solidui.entity.core import JobElement + + +class JobElementDAO(BaseDAO[JobElement]): + model_cls = JobElement diff --git a/solidui/daos/job_element_page.py b/solidui/daos/job_element_page.py new file mode 100644 index 00000000..8c443a21 --- /dev/null +++ b/solidui/daos/job_element_page.py @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Optional +from solidui.daos.base import BaseDAO +from solidui.daos.exceptions import DAONotFound +from solidui.entity.core import JobElementPage + + +class JobElementPageDAO(BaseDAO[JobElementPage]): + model_cls = JobElementPage diff --git a/solidui/daos/job_page.py b/solidui/daos/job_page.py new file mode 100644 index 00000000..92cad84f --- /dev/null +++ b/solidui/daos/job_page.py @@ -0,0 +1,38 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Optional +from solidui.daos.base import BaseDAO +from solidui.daos.exceptions import DAONotFound +from solidui.entity.core import JobPage +from solidui.extensions import db + +class JobPageDAO(BaseDAO[JobPage]): + model_cls = JobPage + + @classmethod + def query_by_name(cls, name, project_id) -> JobPage: + return db.session.query(JobPage).filter_by(name=name, project_id=project_id).first() + + @classmethod + def delete_by_project_id(cls, project_id) -> None: + db.session.query(JobPage).filter_by(project_id=project_id).delete() + db.session.commit() + + @classmethod + def query_job_page_parent_ids(cls, parent_id) -> list[JobPage]: + return db.session.query(JobPage).filter_by(parent_id=parent_id).all() diff --git a/solidui/datasource_plugin/mysql/__init__.py b/solidui/daos/model_type.py similarity index 70% rename from solidui/datasource_plugin/mysql/__init__.py rename to solidui/daos/model_type.py index 2aef8ab2..fc4eaf49 100644 --- a/solidui/datasource_plugin/mysql/__init__.py +++ b/solidui/daos/model_type.py @@ -10,4 +10,17 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. + +from __future__ import annotations + +from solidui.daos.base import BaseDAO +from solidui.entity.core import ModelType + + +class ModelTypeDAO(BaseDAO[ModelType]): + model_cls = ModelType + + @classmethod + def get_list(cls) -> list[ModelType]: + return super().find_all() diff --git a/solidui/datasource_plugin/base.py b/solidui/datasource_plugin/base.py index 2aef8ab2..0b7ce67f 100644 --- a/solidui/datasource_plugin/base.py +++ b/solidui/datasource_plugin/base.py @@ -10,4 +10,115 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. +from __future__ import annotations +from abc import abstractmethod, ABC +from concurrent.futures import ThreadPoolExecutor +import MySQLdb + + +# Base classes +class BaseJdbcClient(ABC): + def __init__(self, conn): + self.conn = conn + + @abstractmethod + def get_databases(self): + pass + + @abstractmethod + def get_tables(self, database): + pass + + @abstractmethod + def run_query(self, sql): + pass + + @abstractmethod + def generate_select_all_data_sql(self, database, table_name): + pass + + def close(self): + self.conn.close() + + +class JdbcClientFactory(ABC): + executor = ThreadPoolExecutor(max_workers=10) + + @staticmethod + def create_client(db_type, host, port, username, password, database, extra_params=None) -> BaseJdbcClient: + if extra_params is None: + extra_params = {} + if db_type == 'mysql': + conn = MySQLdb.connect( + host=host, + port=port, + user=username, + passwd=password, + db=database, + **extra_params + ) + + return MySQLClient(conn) + else: + raise ValueError("Unsupported database type") + + @staticmethod + def run_query(client, sql: str): + def query(): + try: + return client.run_query(sql) + finally: + client.close() + + future = JdbcClientFactory.executor.submit(query) + return future.result() + + @staticmethod + def get_databases(client): + def query(): + try: + return client.get_databases() + finally: + client.close() + + future = JdbcClientFactory.executor.submit(query) + return future.result() + + @staticmethod + def get_tables(client, database: str): + def query(): + try: + return client.get_tables(database) + finally: + client.close() + + future = JdbcClientFactory.executor.submit(query) + return future.result() + + + +class MySQLClient(BaseJdbcClient): + def get_databases(self): + cursor = self.conn.cursor() + cursor.execute("SHOW DATABASES") + return [db[0] for db in cursor] + + def get_tables(self, database): + cursor = self.conn.cursor() + cursor.execute(f"SHOW TABLES FROM {database}") + return [table[0] for table in cursor] + + def run_query(self, sql): + cursor = self.conn.cursor() + cursor.execute(sql) + column_names = [col[0] for col in cursor.description] + rows = [column_names] # First row is column names + + for row in cursor.fetchall(): + rows.append([str(col) if col is not None else None for col in row]) + + return rows + + def generate_select_all_data_sql(self, database, table_name): + return f"SELECT * FROM {database}.{table_name}" diff --git a/solidui/views/job/api.py b/solidui/views/job/api.py index e69de29b..0980e302 100644 --- a/solidui/views/job/api.py +++ b/solidui/views/job/api.py @@ -0,0 +1,56 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import logging +from flask import request + + +from solidui.daos.exceptions import DAONotFound +from solidui.errors import SolidUIErrorType +from solidui.solidui_typing import FlaskResponse +from solidui.views.base_api import BaseSolidUIApi +from flask_appbuilder.api import expose, safe + + +logger = logging.getLogger(__name__) + + +class JobRestApi(BaseSolidUIApi): + route_base = "/solidui/job" + + @expose('/save/page', methods=['POST']) + @safe + def save_page(self) -> FlaskResponse: + job_element_page_data = request.json + # Logic to create job element page + # Return appropriate response + return self.response_format() + + @expose('/update/page', methods=['PUT']) + @safe + def update_job_page(self) -> FlaskResponse: + job_element_page_data = request.json + # Logic to update job element page + # Return appropriate response + return self.response_format() + + @expose('/query/page', methods=['GET']) + @safe + def query_job_page(self) -> FlaskResponse: + project_id = request.args.get('projectId') + page_id = request.args.get('pageId') + return self.response_format() + diff --git a/solidui/views/job_page/api.py b/solidui/views/job_page/api.py index 2aef8ab2..ff02ef0a 100644 --- a/solidui/views/job_page/api.py +++ b/solidui/views/job_page/api.py @@ -10,4 +10,51 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. + +import json +import logging +from flask import request + +from solidui.daos.exceptions import DAONotFound +from solidui.errors import SolidUIErrorType +from solidui.solidui_typing import FlaskResponse +from solidui.views.base_api import BaseSolidUIApi +from flask_appbuilder.api import expose, safe + +logger = logging.getLogger(__name__) + + +class JobPageRestApi(BaseSolidUIApi): + route_base = "/solidui/job/page" + + @expose('', methods=('POST')) + @safe + def create_job_page(self) -> FlaskResponse: + # Extract data from request + + return self.response_format() + + @expose('/', methods=('PUT')) + @safe + def update_job_page(self, id) -> FlaskResponse: + """ + update job page + """ + return self.response_format() + + @expose('/', methods=('DELETE')) + @safe + def delete_job_page(self, id) -> FlaskResponse: + """ + delete job page + """ + return self.response_format() + + @expose('/query/', methods=('GET')) + @safe + def get_job_pages(self, project_id) -> FlaskResponse: + """ + get job page + """ + return self.response_format() diff --git a/solidui/views/metadata/api.py b/solidui/views/metadata/api.py index 2aef8ab2..6ec51340 100644 --- a/solidui/views/metadata/api.py +++ b/solidui/views/metadata/api.py @@ -10,4 +10,221 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. +import json +import logging +from flask import request + +from solidui.daos.datasource import DataSourceDAO +from solidui.daos.datasource_type import DataSourceTypeDAO +from solidui.daos.exceptions import DAONotFound +from solidui.datasource_plugin.base import JdbcClientFactory +from solidui.errors import SolidUIErrorType +from solidui.solidui_typing import FlaskResponse +from solidui.views.base_api import BaseSolidUIApi +from flask_appbuilder.api import expose, safe + + +logger = logging.getLogger(__name__) + + +class MetadataQueryRestApi(BaseSolidUIApi): + route_base = "/solidui/metadataQuery" + + @expose('/queryDatabases', methods=("GET",)) + @safe + def get_databases(self) -> FlaskResponse: + data_source_name = request.args.get('dataSourceName') + # Call the service method to get databases. You need to implement this logic + + data_source = DataSourceDAO.get_data_source_name(data_source_name) + if data_source is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_ERROR) + + data_source_type = DataSourceTypeDAO.get_id(data_source.datasource_type_id) + if data_source_type is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_TYPE_ERROR) + + # Parse the JSON parameters + params = json.loads(data_source.parameter) + + # Create a JDBC client + jdbc_client = JdbcClientFactory.create_client( + db_type=data_source_type.name.lower(), # Assuming MySQL for example + host=params.get("host"), + port=params.get("port"), + username=params.get("username"), + password=params.get("password"), + database=params.get("database"), + extra_params=params.get("params", {}) + ) + + try: + # Retrieve and return the list of databases + databases = JdbcClientFactory.get_databases(jdbc_client) + return self.response_format(data=databases) + + except DAONotFound as ex: + logger.exception(ex) + return self.handle_error(SolidUIErrorType.QUERY_METADATA_DB_ERROR) + + + + @expose('/queryTables', methods=("GET",)) + @safe + def get_tables(self) -> FlaskResponse: + data_source_name = request.args.get('dataSourceName') + database = request.args.get('database') + # Call the service method to get tables. You need to implement this logic + + data_source = DataSourceDAO.get_data_source_name(data_source_name) + if data_source is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_ERROR) + + data_source_type = DataSourceTypeDAO.get_id(data_source.datasource_type_id) + if data_source_type is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_TYPE_ERROR) + + # Parse the JSON parameters + params = json.loads(data_source.parameter) + + # Create a JDBC client + jdbc_client = JdbcClientFactory.create_client( + db_type=data_source_type.name, # Assuming MySQL for example + host=params.get("host"), + port=params.get("port"), + username=params.get("username"), + password=params.get("password"), + database=params.get("database"), + extra_params=params.get("params", {}) + ) + + try: + # Retrieve and return the list of databases + tables = JdbcClientFactory.get_tables(jdbc_client, database) + return self.response_format(data=tables) + + except DAONotFound as ex: + logger.exception(ex) + return self.handle_error(SolidUIErrorType.QUERY_METADATA_TABLE_ERROR) + + @expose('/queryTableData', methods=("GET",)) + @safe + def get_table_data(self) -> FlaskResponse: + data_source_name = request.args.get('dataSourceName') + database = request.args.get('database') + table_name = request.args.get('tableName') + type_name = request.args.get('typeName', default=None) + # Call the service method to get table data. You need to implement this logic + data_source = DataSourceDAO.get_data_source_name(data_source_name) + if data_source is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_ERROR) + + data_source_type = DataSourceTypeDAO.get_id(data_source.datasource_type_id) + if data_source_type is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_TYPE_ERROR) + + # Parse the JSON parameters + params = json.loads(data_source.parameter) + + # Create a JDBC client + jdbc_client = JdbcClientFactory.create_client( + db_type=data_source_type.name, # Assuming MySQL for example + host=params.get("host"), + port=params.get("port"), + username=params.get("username"), + password=params.get("password"), + database=params.get("database"), + extra_params=params.get("params", {}) + ) + + select_all_data_sql = jdbc_client.generate_select_all_data_sql(database, table_name) + select_result = JdbcClientFactory.run_query(jdbc_client, select_all_data_sql) + + # Transform the result into the desired format + if not select_result or len(select_result) == 1: + return self.handle_error(SolidUIErrorType.QUERY_METADATA_SQL_ERROR) + + field_value_results = [] + columns = select_result[0] + for row in select_result[1:]: + field_value_results.append(dict(zip(columns, row))) + + return self.response_format(data=field_value_results) + + @expose('/querySql', methods=("GET",)) + @safe + def query_sql(self) -> FlaskResponse: + data_source_name = request.args.get('dataSourceName') + sql = request.args.get('sql') + + # Call the service method to get table data. You need to implement this logic + data_source = DataSourceDAO.get_data_source_name(data_source_name) + if data_source is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_ERROR) + + data_source_type = DataSourceTypeDAO.get_id(data_source.datasource_type_id) + if data_source_type is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_TYPE_ERROR) + + # Parse the JSON parameters + params = json.loads(data_source.parameter) + + # Create a JDBC client + jdbc_client = JdbcClientFactory.create_client( + db_type=data_source_type.name, # Assuming MySQL for example + host=params.get("host"), + port=params.get("port"), + username=params.get("username"), + password=params.get("password"), + database=params.get("database"), + extra_params=params.get("params", {}) + ) + + try: + + data_list = JdbcClientFactory.run_query(jdbc_client, sql) + return self.response_format(data=data_list) + + except DAONotFound as ex: + logger.exception(ex) + return self.handle_error(SolidUIErrorType.QUERY_METADATA_SQL_ERROR) + + @expose('/querySql/id', methods=("GET",)) + @safe + def query_sql_by_id(self) -> FlaskResponse: + data_source_id = request.args.get('dataSourceId', type=int) + sql = request.args.get('sql') + # Call the service method to execute the SQL query by ID. You need to implement this logic + + # Call the service method to get table data. You need to implement this logic + data_source = DataSourceDAO.get_data_source_id(data_source_id) + if data_source is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_ERROR) + + data_source_type = DataSourceTypeDAO.get_id(data_source.datasource_type_id) + if data_source_type is None: + return self.handle_error(SolidUIErrorType.QUERY_DATASOURCE_TYPE_ERROR) + + # Parse the JSON parameters + params = json.loads(data_source.parameter) + + # Create a JDBC client + jdbc_client = JdbcClientFactory.create_client( + db_type=data_source_type.name, # Assuming MySQL for example + host=params.get("host"), + port=params.get("port"), + username=params.get("username"), + password=params.get("password"), + database=params.get("database"), + extra_params=params.get("params", {}) + ) + + try: + + data_list = JdbcClientFactory.run_query(jdbc_client, sql) + return self.response_format(data=data_list) + + except DAONotFound as ex: + logger.exception(ex) + return self.handle_error(SolidUIErrorType.QUERY_METADATA_SQL_ERROR) diff --git a/solidui/views/model/api.py b/solidui/views/model/api.py index 2aef8ab2..887f79b8 100644 --- a/solidui/views/model/api.py +++ b/solidui/views/model/api.py @@ -10,4 +10,43 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. + +import json +import logging +from flask import request + + +from solidui.daos.exceptions import DAONotFound +from solidui.daos.model_type import ModelTypeDAO +from solidui.errors import SolidUIErrorType +from solidui.solidui_typing import FlaskResponse +from solidui.views.base_api import BaseSolidUIApi +from flask_appbuilder.api import expose, safe + +from solidui.views.model.schemas import ModelKeyVO + +logger = logging.getLogger(__name__) + + +class ModelRestApi(BaseSolidUIApi): + route_base = "/solidui/models" + + + ## soliduimodelui/webapp + + @expose('/list', methods=("GET",)) + @safe + def get_model_list(self) -> FlaskResponse: + """ + keys list + """ + model_types = ModelTypeDAO.get_list() + model_key_vos = [] + + for m in model_types: + model_key_vos.append(ModelKeyVO(m.id, f"{m.name}_{m.code}", m.type_name)) + + return self.response_format(data=model_key_vos) + + diff --git a/solidui/views/model/schemas.py b/solidui/views/model/schemas.py new file mode 100644 index 00000000..ba8c8563 --- /dev/null +++ b/solidui/views/model/schemas.py @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema + +from solidui.entity.core import ModelType +from marshmallow_sqlalchemy.fields import fields + + +class ModelKeyVO: + def __init__(self, id, name, type_name): + self.id = id + self.name = name + self.type_name = type_name