This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch sl-3-api-integration in repository https://gitbox.apache.org/repos/asf/superset.git
commit 20d1480db50e199aae87d1cd589ca200430a22e7 Author: Beto Dealmeida <[email protected]> AuthorDate: Mon Feb 2 17:29:51 2026 -0500 feat(semantic-layer): integrate semantic views with explore API Update views and APIs to support semantic layer explorables: - Add UUID support for datasource IDs in views and utils - Update explore command to handle semantic views - Extend datasource DAO to resolve semantic view UUIDs - Update explore API parameters for semantic layer support Co-Authored-By: Claude Opus 4.5 <[email protected]> --- superset/commands/explore/get.py | 15 ++++++++++----- superset/commands/explore/parameters.py | 4 ++-- superset/daos/datasource.py | 4 +++- superset/explore/api.py | 2 +- superset/views/core.py | 19 ++++++++++--------- superset/views/utils.py | 23 +++++++++++++++-------- 6 files changed, 41 insertions(+), 26 deletions(-) diff --git a/superset/commands/explore/get.py b/superset/commands/explore/get.py index 78142eb5ec1..7a99d5a2a42 100644 --- a/superset/commands/explore/get.py +++ b/superset/commands/explore/get.py @@ -18,6 +18,7 @@ import contextlib import logging from abc import ABC from typing import Any, cast, Optional +from uuid import UUID from flask import request from flask_babel import lazy_gettext as _ @@ -100,21 +101,21 @@ class GetExploreCommand(BaseCommand, ABC): use_slice_data=True, initial_form_data=initial_form_data, ) + ds_id: int | UUID | None = None try: - self._datasource_id, self._datasource_type = get_datasource_info( + ds_id, self._datasource_type = get_datasource_info( self._datasource_id, self._datasource_type, form_data ) except SupersetException: - self._datasource_id = None # fallback unknown datasource to table type self._datasource_type = SqlaTable.type datasource: Optional[BaseDatasource] = None - if self._datasource_id is not None: + if ds_id is not None: with contextlib.suppress(DatasourceNotFound): datasource = DatasourceDAO.get_datasource( - cast(str, self._datasource_type), self._datasource_id + cast(str, self._datasource_type), ds_id ) datasource_name = _("[Missing Dataset]") @@ -124,7 +125,11 @@ class GetExploreCommand(BaseCommand, ABC): security_manager.raise_for_access(datasource=datasource) viz_type = form_data.get("viz_type") - if not viz_type and datasource and datasource.default_endpoint: + if ( + not viz_type + and datasource + and getattr(datasource, "default_endpoint", None) + ): raise WrongEndpointError(redirect=datasource.default_endpoint) form_data["datasource"] = ( diff --git a/superset/commands/explore/parameters.py b/superset/commands/explore/parameters.py index 1aa5418d626..d5457e102e6 100644 --- a/superset/commands/explore/parameters.py +++ b/superset/commands/explore/parameters.py @@ -15,13 +15,13 @@ # specific language governing permissions and limitations # under the License. from dataclasses import dataclass -from typing import Optional +from typing import Optional, Union @dataclass class CommandParameters: permalink_key: Optional[str] form_data_key: Optional[str] - datasource_id: Optional[int] + datasource_id: Optional[Union[int, str]] datasource_type: Optional[str] slice_id: Optional[int] diff --git a/superset/daos/datasource.py b/superset/daos/datasource.py index 308785f625e..f5030f35353 100644 --- a/superset/daos/datasource.py +++ b/superset/daos/datasource.py @@ -28,6 +28,7 @@ from superset.daos.exceptions import ( DatasourceValueIsIncorrect, ) from superset.models.sql_lab import Query, SavedQuery +from superset.semantic_layers.models import SemanticView from superset.utils.core import DatasourceType logger = logging.getLogger(__name__) @@ -40,13 +41,14 @@ class DatasourceDAO(BaseDAO[Datasource]): DatasourceType.TABLE: SqlaTable, DatasourceType.QUERY: Query, DatasourceType.SAVEDQUERY: SavedQuery, + DatasourceType.SEMANTIC_VIEW: SemanticView, } @classmethod def get_datasource( cls, datasource_type: Union[DatasourceType, str], - database_id_or_uuid: int | str, + database_id_or_uuid: int | str | uuid.UUID, ) -> Datasource: if datasource_type not in cls.sources: raise DatasourceTypeNotSupportedError() diff --git a/superset/explore/api.py b/superset/explore/api.py index e16b083feb8..a9b66d468c9 100644 --- a/superset/explore/api.py +++ b/superset/explore/api.py @@ -109,7 +109,7 @@ class ExploreRestApi(BaseSupersetApi): params = CommandParameters( permalink_key=request.args.get("permalink_key", type=str), form_data_key=request.args.get("form_data_key", type=str), - datasource_id=request.args.get("datasource_id", type=int), + datasource_id=request.args.get("datasource_id"), datasource_type=request.args.get("datasource_type", type=str), slice_id=request.args.get("slice_id", type=int), ) diff --git a/superset/views/core.py b/superset/views/core.py index cbe722cc92c..fde9f21efa9 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -24,6 +24,7 @@ import re from datetime import datetime from typing import Any, Callable, cast from urllib import parse +from uuid import UUID from flask import ( abort, @@ -268,7 +269,7 @@ class Superset(BaseSupersetView): @check_resource_permissions(check_datasource_perms) @deprecated(eol_version="5.0.0") def explore_json( - self, datasource_type: str | None = None, datasource_id: int | None = None + self, datasource_type: str | None = None, datasource_id: int | str | None = None ) -> FlaskResponse: """Serves all request that GET or POST form_data @@ -302,7 +303,7 @@ class Superset(BaseSupersetView): form_data = get_form_data()[0] try: - datasource_id, datasource_type = get_datasource_info( + ds_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data ) force = request.args.get("force") == "true" @@ -316,7 +317,7 @@ class Superset(BaseSupersetView): with contextlib.suppress(CacheLoadError): viz_obj = get_viz( datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, + datasource_id=ds_id, form_data=form_data, force_cached=True, force=force, @@ -343,7 +344,7 @@ class Superset(BaseSupersetView): viz_obj = get_viz( datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, + datasource_id=ds_id, form_data=form_data, force=force, ) @@ -407,7 +408,7 @@ class Superset(BaseSupersetView): def explore( # noqa: C901 self, datasource_type: str | None = None, - datasource_id: int | None = None, + datasource_id: int | str | None = None, key: str | None = None, ) -> FlaskResponse: if request.method == "GET": @@ -451,21 +452,21 @@ class Superset(BaseSupersetView): query_context = request.form.get("query_context") + ds_id: int | UUID | None = None try: - datasource_id, datasource_type = get_datasource_info( + ds_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data ) except SupersetException: - datasource_id = None # fallback unknown datasource to table type datasource_type = SqlaTable.type datasource: BaseDatasource | None = None - if datasource_id is not None: + if ds_id is not None: with contextlib.suppress(DatasetNotFoundError): datasource = DatasourceDAO.get_datasource( DatasourceType("table"), - datasource_id, + ds_id, ) datasource_name = datasource.name if datasource else _("[Missing Dataset]") diff --git a/superset/views/utils.py b/superset/views/utils.py index 1ec9e2a54ac..57e79b93cf0 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -20,6 +20,7 @@ from collections import defaultdict from functools import wraps from typing import Any, Callable, DefaultDict, Optional, Union from urllib import parse +from uuid import UUID import msgpack import pyarrow as pa @@ -163,7 +164,7 @@ def get_permissions( def get_viz( form_data: FormData, datasource_type: str, - datasource_id: int, + datasource_id: int | UUID, force: bool = False, force_cached: bool = False, ) -> BaseViz: @@ -272,8 +273,10 @@ def add_sqllab_custom_filters(form_data: dict[Any, Any]) -> Any: def get_datasource_info( - datasource_id: Optional[int], datasource_type: Optional[str], form_data: FormData -) -> tuple[int, Optional[str]]: + datasource_id: int | str | None, + datasource_type: Optional[str], + form_data: FormData, +) -> tuple[int | UUID, Optional[str]]: """ Compatibility layer for handling of datasource info @@ -300,8 +303,12 @@ def get_datasource_info( _("The dataset associated with this chart no longer exists") ) - datasource_id = int(datasource_id) - return datasource_id, datasource_type + # Convert datasource_id to appropriate type + if isinstance(datasource_id, int): + return datasource_id, datasource_type + if datasource_id.isdigit(): + return int(datasource_id), datasource_type + return UUID(datasource_id), datasource_type def apply_display_max_row_limit( @@ -483,7 +490,7 @@ def check_explore_cache_perms(_self: Any, cache_key: str) -> None: def check_datasource_perms( _self: Any, datasource_type: Optional[str] = None, - datasource_id: Optional[int] = None, + datasource_id: int | str | None = None, **kwargs: Any, ) -> None: """ @@ -500,7 +507,7 @@ def check_datasource_perms( form_data = kwargs["form_data"] if "form_data" in kwargs else get_form_data()[0] try: - datasource_id, datasource_type = get_datasource_info( + ds_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data ) except SupersetException as ex: @@ -524,7 +531,7 @@ def check_datasource_perms( try: viz_obj = get_viz( datasource_type=datasource_type, - datasource_id=datasource_id, + datasource_id=ds_id, form_data=form_data, force=False, )
