feat: API for semantic views

This commit is contained in:
Beto Dealmeida
2026-02-10 19:24:36 -05:00
parent 85175557f3
commit 3730bc0f20
11 changed files with 786 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
# 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.

View File

@@ -0,0 +1,48 @@
# 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 superset.commands.semantic_layer.exceptions import (
SemanticViewForbiddenError,
SemanticViewInvalidError,
SemanticViewNotFoundError,
SemanticViewUpdateFailedError,
)
def test_semantic_view_not_found_error() -> None:
"""Test SemanticViewNotFoundError has correct status and message."""
error = SemanticViewNotFoundError()
assert error.status == 404
assert str(error.message) == "Semantic view does not exist"
def test_semantic_view_forbidden_error() -> None:
"""Test SemanticViewForbiddenError has correct message."""
error = SemanticViewForbiddenError()
assert str(error.message) == "Changing this semantic view is forbidden"
def test_semantic_view_invalid_error() -> None:
"""Test SemanticViewInvalidError has correct message."""
error = SemanticViewInvalidError()
assert str(error.message) == "Semantic view parameters are invalid."
def test_semantic_view_update_failed_error() -> None:
"""Test SemanticViewUpdateFailedError has correct message."""
error = SemanticViewUpdateFailedError()
assert str(error.message) == "Semantic view could not be updated."

View File

@@ -0,0 +1,104 @@
# 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 unittest.mock import MagicMock
import pytest
from pytest_mock import MockerFixture
from superset.commands.semantic_layer.exceptions import (
SemanticViewForbiddenError,
SemanticViewNotFoundError,
)
from superset.commands.semantic_layer.update import UpdateSemanticViewCommand
from superset.exceptions import SupersetSecurityException
def test_update_semantic_view_success(mocker: MockerFixture) -> None:
"""Test successful update of a semantic view."""
mock_model = MagicMock()
mock_model.id = 1
dao = mocker.patch(
"superset.commands.semantic_layer.update.SemanticViewDAO",
)
dao.find_by_id.return_value = mock_model
dao.update.return_value = mock_model
mocker.patch(
"superset.commands.semantic_layer.update.security_manager",
)
data = {"description": "Updated", "cache_timeout": 300}
result = UpdateSemanticViewCommand(1, data).run()
assert result == mock_model
dao.find_by_id.assert_called_once_with(1)
dao.update.assert_called_once_with(mock_model, attributes=data)
def test_update_semantic_view_not_found(mocker: MockerFixture) -> None:
"""Test that SemanticViewNotFoundError is raised when model is missing."""
dao = mocker.patch(
"superset.commands.semantic_layer.update.SemanticViewDAO",
)
dao.find_by_id.return_value = None
with pytest.raises(SemanticViewNotFoundError):
UpdateSemanticViewCommand(999, {"description": "test"}).run()
def test_update_semantic_view_forbidden(mocker: MockerFixture) -> None:
"""Test that SemanticViewForbiddenError is raised on ownership failure."""
mock_model = MagicMock()
dao = mocker.patch(
"superset.commands.semantic_layer.update.SemanticViewDAO",
)
dao.find_by_id.return_value = mock_model
sm = mocker.patch(
"superset.commands.semantic_layer.update.security_manager",
)
# Use a regular MagicMock for raise_for_ownership to avoid AsyncMock issues
sm.raise_for_ownership = MagicMock(
side_effect=SupersetSecurityException(MagicMock()),
)
with pytest.raises(SemanticViewForbiddenError):
UpdateSemanticViewCommand(1, {"description": "test"}).run()
def test_update_semantic_view_copies_data(mocker: MockerFixture) -> None:
"""Test that the command copies input data and does not mutate it."""
mock_model = MagicMock()
dao = mocker.patch(
"superset.commands.semantic_layer.update.SemanticViewDAO",
)
dao.find_by_id.return_value = mock_model
dao.update.return_value = mock_model
mocker.patch(
"superset.commands.semantic_layer.update.security_manager",
)
original_data = {"description": "Original"}
UpdateSemanticViewCommand(1, original_data).run()
# The original dict should not have been modified
assert original_data == {"description": "Original"}