mirror of
https://github.com/apache/superset.git
synced 2026-07-18 12:45:44 +00:00
Addresses @EnxDev's review on #41133: - Soft timeouts now abort the export instead of being caught per chart: the per-chart SoftTimeLimitExceeded handler (and screenshot.py's broad except) re-raise, so the outer handler emails a failure and runs cleanup rather than running to the hard limit (leaking temp files, holding the lock). Removes the now-dead ERROR_TIMEOUT reason. - Concurrency guard uses a shared, atomic DistributedLock (Redis when configured, metadata DB otherwise) instead of cache_manager.cache, which is a no-op under the default NullCache and process-local under SimpleCache. The lock is released if apply_async fails so a broker outage can't block exports until the TTL expires. - boto3 is declared via a new `excel-export` optional extra; superset.utils.s3 raises an actionable install hint when it is missing. - "Export Images to Excel" is gated on the webdriver screenshot feature flags (it renders via the headless webdriver); documents EXCEL_EXPORT_TABLE_VIZ_TYPES and the image mode in the config docs and UPDATING.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
# 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
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from superset.utils import s3
|
|
|
|
|
|
@patch("boto3.client")
|
|
@patch("superset.utils.s3.current_app")
|
|
def test_upload_file_to_s3(mock_app: MagicMock, mock_client_fn: MagicMock) -> None:
|
|
mock_app.config = {"EXCEL_EXPORT_S3_CLIENT_KWARGS": {}}
|
|
client = mock_client_fn.return_value
|
|
|
|
s3.upload_file_to_s3("exports/out.xlsx", "my-bucket", "exports/1/abc.xlsx")
|
|
|
|
mock_client_fn.assert_called_once_with("s3")
|
|
client.upload_file.assert_called_once_with(
|
|
"exports/out.xlsx", "my-bucket", "exports/1/abc.xlsx"
|
|
)
|
|
|
|
|
|
@patch("boto3.client")
|
|
@patch("superset.utils.s3.current_app")
|
|
def test_client_kwargs_passthrough(
|
|
mock_app: MagicMock, mock_client_fn: MagicMock
|
|
) -> None:
|
|
mock_app.config = {
|
|
"EXCEL_EXPORT_S3_CLIENT_KWARGS": {
|
|
"endpoint_url": "http://minio:9000",
|
|
"region_name": "us-east-1",
|
|
}
|
|
}
|
|
|
|
s3.upload_file_to_s3("exports/out.xlsx", "my-bucket", "k")
|
|
|
|
mock_client_fn.assert_called_once_with(
|
|
"s3", endpoint_url="http://minio:9000", region_name="us-east-1"
|
|
)
|
|
|
|
|
|
def test_importing_module_does_not_require_boto3() -> None:
|
|
# Regression: importing this module (which app startup does via the dashboard
|
|
# API) must not require boto3, since it is only an optional install.
|
|
import importlib
|
|
|
|
with patch.dict(sys.modules, {"boto3": None}):
|
|
importlib.reload(s3)
|
|
# Reload again with boto3 available so later tests see the normal module.
|
|
importlib.reload(s3)
|
|
|
|
|
|
def test_get_s3_client_missing_boto3_raises_actionable_error() -> None:
|
|
# Simulate a production install without boto3: the lazy import fails and we
|
|
# surface an install hint instead of a bare ModuleNotFoundError.
|
|
with (
|
|
patch.dict(sys.modules, {"boto3": None}),
|
|
pytest.raises(ImportError, match="excel-export"),
|
|
):
|
|
s3._get_s3_client()
|
|
|
|
|
|
@patch("boto3.client")
|
|
@patch("superset.utils.s3.current_app")
|
|
def test_generate_presigned_url(mock_app: MagicMock, mock_client_fn: MagicMock) -> None:
|
|
mock_app.config = {"EXCEL_EXPORT_S3_CLIENT_KWARGS": {}}
|
|
client = mock_client_fn.return_value
|
|
client.generate_presigned_url.return_value = "https://signed.example/abc"
|
|
|
|
url = s3.generate_presigned_url("my-bucket", "exports/1/abc.xlsx", 86400)
|
|
|
|
assert url == "https://signed.example/abc"
|
|
client.generate_presigned_url.assert_called_once_with(
|
|
"get_object",
|
|
Params={"Bucket": "my-bucket", "Key": "exports/1/abc.xlsx"},
|
|
ExpiresIn=86400,
|
|
)
|