mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(dbc ui): Adding Google Sheets Dynamic Form (#15801)
* feat: Make Google Sheets Dyanmic (#15576) * first draft * second draft * added tests * first draft * added table_catalog * remove console.log * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * revisions * save this for now * working form * save disable on public sheets * refactor somethings * saving this for now * working edit * add back query to schema * working add * fix styling * fixing x * fix linting * prettier * fix some type issues * more lint fixes * remove unused dependency * fix linint * fix validation * pylint bypass * pylint bypass * fix this * fix mypy * yerp * fix test * fix test * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * wrap add sheets * fix linting issues * fix unit test * ignore typing * fix editting and paste issues * remove query * fix this * fix test * add test back * fix error messaging * update url messaging on error * change error type * Update superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx Co-authored-by: Beto Dealmeida <roberto@dealmeida.net> * add errors for sheets with no name * fix * fix messaging for gsheets * stop pylint * update line Co-authored-by: AAfghahi <48933336+AAfghahi@users.noreply.github.com> Co-authored-by: Arash <arash.afghahi@gmail.com> Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
This commit is contained in:
@@ -19,13 +19,18 @@ import re
|
||||
from contextlib import closing
|
||||
from typing import Any, Dict, List, Optional, Pattern, Tuple, TYPE_CHECKING
|
||||
|
||||
from apispec import APISpec
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from flask import g
|
||||
from flask_babel import gettext as __
|
||||
from marshmallow import fields, Schema
|
||||
from marshmallow.exceptions import ValidationError
|
||||
from sqlalchemy.engine import create_engine
|
||||
from sqlalchemy.engine.url import URL
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from superset import security_manager
|
||||
from superset.databases.schemas import encrypted_field_properties
|
||||
from superset.db_engine_specs.sqlite import SqliteEngineSpec
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
|
||||
@@ -35,11 +40,16 @@ if TYPE_CHECKING:
|
||||
|
||||
SYNTAX_ERROR_REGEX = re.compile('SQLError: near "(?P<server_error>.*?)": syntax error')
|
||||
|
||||
ma_plugin = MarshmallowPlugin()
|
||||
|
||||
|
||||
class GSheetsParametersSchema(Schema):
|
||||
catalog = fields.Dict()
|
||||
|
||||
|
||||
class GSheetsParametersType(TypedDict):
|
||||
credentials_info: Dict[str, Any]
|
||||
query: Dict[str, Any]
|
||||
table_catalog: Dict[str, str]
|
||||
catalog: Dict[str, str]
|
||||
|
||||
|
||||
class GSheetsEngineSpec(SqliteEngineSpec):
|
||||
@@ -50,6 +60,10 @@ class GSheetsEngineSpec(SqliteEngineSpec):
|
||||
allows_joins = True
|
||||
allows_subqueries = True
|
||||
|
||||
parameters_schema = GSheetsParametersSchema()
|
||||
default_driver = "apsw"
|
||||
sqlalchemy_uri_placeholder = "gsheets://"
|
||||
|
||||
custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = {
|
||||
SYNTAX_ERROR_REGEX: (
|
||||
__(
|
||||
@@ -87,16 +101,64 @@ class GSheetsEngineSpec(SqliteEngineSpec):
|
||||
|
||||
return {"metadata": metadata["extra"]}
|
||||
|
||||
@classmethod
|
||||
def build_sqlalchemy_uri(
|
||||
cls,
|
||||
_: GSheetsParametersType,
|
||||
encrypted_extra: Optional[ # pylint: disable=unused-argument
|
||||
Dict[str, Any]
|
||||
] = None,
|
||||
) -> str: # pylint: disable=unused-variable
|
||||
|
||||
return "gsheets://"
|
||||
|
||||
@classmethod
|
||||
def get_parameters_from_uri(
|
||||
cls, encrypted_extra: Optional[Dict[str, str]] = None,
|
||||
) -> Any:
|
||||
# Building parameters from encrypted_extra and uri
|
||||
if encrypted_extra:
|
||||
return {**encrypted_extra}
|
||||
|
||||
raise ValidationError("Invalid service credentials")
|
||||
|
||||
@classmethod
|
||||
def parameters_json_schema(cls) -> Any:
|
||||
"""
|
||||
Return configuration parameters as OpenAPI.
|
||||
"""
|
||||
if not cls.parameters_schema:
|
||||
return None
|
||||
|
||||
spec = APISpec(
|
||||
title="Database Parameters",
|
||||
version="1.0.0",
|
||||
openapi_version="3.0.0",
|
||||
plugins=[ma_plugin],
|
||||
)
|
||||
|
||||
ma_plugin.init_spec(spec)
|
||||
ma_plugin.converter.add_attribute_function(encrypted_field_properties)
|
||||
spec.components.schema(cls.__name__, schema=cls.parameters_schema)
|
||||
return spec.to_dict()["components"]["schemas"][cls.__name__]
|
||||
|
||||
@classmethod
|
||||
def validate_parameters(
|
||||
cls, parameters: GSheetsParametersType,
|
||||
) -> List[SupersetError]:
|
||||
errors: List[SupersetError] = []
|
||||
|
||||
credentials_info = parameters.get("credentials_info")
|
||||
table_catalog = parameters.get("table_catalog", {})
|
||||
table_catalog = parameters.get("catalog", {})
|
||||
|
||||
if not table_catalog:
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message="URL is required",
|
||||
error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
|
||||
level=ErrorLevel.WARNING,
|
||||
extra={"invalid": ["catalog"], "name": "", "url": ""},
|
||||
),
|
||||
)
|
||||
return errors
|
||||
|
||||
# We need a subject in case domain wide delegation is set, otherwise the
|
||||
@@ -110,17 +172,27 @@ class GSheetsEngineSpec(SqliteEngineSpec):
|
||||
)
|
||||
conn = engine.connect()
|
||||
for name, url in table_catalog.items():
|
||||
|
||||
if not name:
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message="Sheet name is required",
|
||||
error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
|
||||
level=ErrorLevel.WARNING,
|
||||
extra={"invalid": [], "name": name, "url": url},
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
results = conn.execute(f'SELECT * FROM "{url}" LIMIT 1')
|
||||
results.fetchall()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
errors.append(
|
||||
SupersetError(
|
||||
message=f"Unable to connect to spreadsheet {name} at {url}",
|
||||
message="URL could not be identified",
|
||||
error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
|
||||
level=ErrorLevel.WARNING,
|
||||
extra={"name": name, "url": url},
|
||||
extra={"invalid": ["catalog"], "name": name, "url": url},
|
||||
),
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
Reference in New Issue
Block a user