Small fixes

This commit is contained in:
Beto Dealmeida
2026-04-16 18:55:48 -04:00
parent 5d6ce82651
commit 0d09ecaae1
7 changed files with 5097 additions and 5405 deletions

View File

@@ -105,7 +105,13 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True} FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
"SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501 WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks. # The base URL for the email report hyperlinks.

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,9 @@ class GetCombinedDatasourceListCommand(BaseCommand):
source_type, name_filter, sql_filter, type_filter = self._parse_filters(filters) source_type, name_filter, sql_filter, type_filter = self._parse_filters(filters)
source_type = self._resolve_source_type(source_type, sql_filter, type_filter) source_type = self._resolve_source_type(source_type, sql_filter, type_filter)
if source_type == "empty":
return {"count": 0, "result": []}
ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter) ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter)
sv_q = DatasourceDAO.build_semantic_view_query(name_filter) sv_q = DatasourceDAO.build_semantic_view_query(name_filter)
@@ -108,8 +111,18 @@ class GetCombinedDatasourceListCommand(BaseCommand):
sql_filter: bool | None, sql_filter: bool | None,
type_filter: str | None, type_filter: str | None,
) -> str: ) -> str:
"""Narrow source_type based on access flags, sql filter, and type filter.""" """Narrow source_type based on access flags, sql filter, and type filter.
Returns one of: "database", "semantic_layer", "all", or "empty".
"empty" signals that the caller should short-circuit and return no results
(used when the user explicitly requests semantic views but lacks access).
"""
if not self._can_read_semantic_views: if not self._can_read_semantic_views:
# If the user explicitly asked for semantic views but cannot read them,
# return "empty" so the caller yields zero results rather than silently
# falling back to the full dataset list.
if source_type == "semantic_layer" or type_filter == "semantic_view":
return "empty"
return "database" return "database"
if not self._can_read_datasets: if not self._can_read_datasets:
return "semantic_layer" return "semantic_layer"

View File

@@ -22,18 +22,20 @@ from __future__ import annotations
from typing import Any from typing import Any
from sqlalchemy.exc import StatementError from sqlalchemy.exc import StatementError
from superset_core.semantic_layers.daos import ( from superset_core.semantic_layers.daos import (
AbstractSemanticLayerDAO, AbstractSemanticLayerDAO,
AbstractSemanticViewDAO, AbstractSemanticViewDAO,
) )
from superset.daos.base import BaseDAO
from superset.extensions import db from superset.extensions import db
from superset.semantic_layers.models import SemanticLayer, SemanticView from superset.semantic_layers.models import SemanticLayer, SemanticView
from superset.utils import json from superset.utils import json
class SemanticLayerDAO(AbstractSemanticLayerDAO): class SemanticLayerDAO(BaseDAO[SemanticLayer], AbstractSemanticLayerDAO):
# SemanticLayer uses uuid as the primary key
id_column_name = "uuid"
""" """
Data Access Object for SemanticLayer model. Data Access Object for SemanticLayer model.
""" """
@@ -112,7 +114,7 @@ class SemanticLayerDAO(AbstractSemanticLayerDAO):
) )
class SemanticViewDAO(AbstractSemanticViewDAO): class SemanticViewDAO(BaseDAO[SemanticView], AbstractSemanticViewDAO):
"""Data Access Object for SemanticView model.""" """Data Access Object for SemanticView model."""
model_cls = SemanticView model_cls = SemanticView

View File

@@ -56,7 +56,7 @@ from superset.commands.semantic_layer.update import (
UpdateSemanticViewCommand, UpdateSemanticViewCommand,
) )
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.daos.semantic_layer import SemanticLayerDAO, SemanticViewDAO from superset.daos.semantic_layer import SemanticLayerDAO
from superset.datasets.schemas import get_delete_ids_schema from superset.datasets.schemas import get_delete_ids_schema
from superset.models.core import Database from superset.models.core import Database
from superset.semantic_layers.models import SemanticLayer, SemanticView from superset.semantic_layers.models import SemanticLayer, SemanticView
@@ -175,6 +175,11 @@ class SemanticViewRestApi(BaseSupersetModelRestApi):
class_permission_name = "SemanticView" class_permission_name = "SemanticView"
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
include_route_methods = {"put", "post", "delete", "bulk_delete"} include_route_methods = {"put", "post", "delete", "bulk_delete"}
# SemanticViewRestApi exposes only write endpoints, but can_read must be
# declared explicitly so that FAB registers the permission. It is used by
# DatasourceRestApi.combined_list to gate access to semantic views in the
# combined datasource list.
base_permissions = ["can_read", "can_write"]
edit_model_schema = SemanticViewPutSchema() edit_model_schema = SemanticViewPutSchema()
@@ -635,7 +640,7 @@ class SemanticLayerRestApi(BaseSupersetApi):
) )
# Check which views already exist with the same runtime config # Check which views already exist with the same runtime config
existing = SemanticViewDAO.find_by_semantic_layer(str(layer.uuid)) existing = SemanticLayerDAO.get_semantic_views(str(layer.uuid))
existing_keys: set[tuple[str, str]] = set() existing_keys: set[tuple[str, str]] = set()
for v in existing: for v in existing:
config = v.configuration config = v.configuration

View File

@@ -291,11 +291,7 @@ def map_query_object(query_object: ValidatedQueryObject) -> list[SemanticQuery]:
metrics = [all_metrics[metric] for metric in (query_object.metrics or [])] metrics = [all_metrics[metric] for metric in (query_object.metrics or [])]
grain = ( grain = _convert_time_grain(query_object.extras.get("time_grain_sqla"))
_convert_time_grain(query_object.extras["time_grain_sqla"])
if "time_grain_sqla" in query_object.extras
else None
)
dimensions = [ dimensions = [
dimension dimension
for dimension in semantic_view.dimensions for dimension in semantic_view.dimensions
@@ -740,13 +736,17 @@ def _get_group_limit_filters(
return filters if filters else None return filters if filters else None
def _convert_time_grain(time_grain: str) -> Grain | None: def _convert_time_grain(time_grain: str | None) -> Grain | None:
""" """
Convert a time grain string (ISO 8601 duration) to a Grain instance. Convert a time grain string (ISO 8601 duration) to a Grain instance.
Returns None when ``time_grain`` is None or empty (no grain selected).
""" """
if not time_grain:
return None
try: try:
return Grains.get(time_grain) return Grains.get(time_grain)
except (ValueError, isodate.ISO8601Error): except (TypeError, ValueError, isodate.ISO8601Error):
return None return None

View File

@@ -1796,8 +1796,7 @@ def test_get_views(
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO") mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = mock_layer mock_dao.find_by_uuid.return_value = mock_layer
mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO") mock_dao.get_semantic_views.return_value = []
mock_sv_dao.find_by_semantic_layer.return_value = []
response = client.post( response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/views", f"/api/v1/semantic_layer/{test_uuid}/views",
@@ -1834,8 +1833,7 @@ def test_get_views_with_existing(
existing_view.name = "Existing View" existing_view.name = "Existing View"
existing_view.configuration = '{"database": "mydb"}' existing_view.configuration = '{"database": "mydb"}'
mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO") mock_dao.get_semantic_views.return_value = [existing_view]
mock_sv_dao.find_by_semantic_layer.return_value = [existing_view]
response = client.post( response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/views", f"/api/v1/semantic_layer/{test_uuid}/views",
@@ -1915,8 +1913,7 @@ def test_get_views_existing_dict_config(
existing_view.name = "View X" existing_view.name = "View X"
existing_view.configuration = {"key": "val"} # dict, not string existing_view.configuration = {"key": "val"} # dict, not string
mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO") mock_dao.get_semantic_views.return_value = [existing_view]
mock_sv_dao.find_by_semantic_layer.return_value = [existing_view]
response = client.post( response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/views", f"/api/v1/semantic_layer/{test_uuid}/views",