Compare commits

..
Author SHA1 Message Date
Amin Ghadersohi e32a683f6f fix(mcp): handle bare string point_radius_fixed as metric in Deck.gl fallback
Legacy deck_scatter charts store point_radius_fixed as a plain metric key
string (e.g. "count") rather than the dict form {"type":"metric","value":...}.
The frontend isMetricValue() treats any non-numeric string as a valid metric
ref, so the fallback query builder now does the same via _is_metric_ref.
2026-05-27 21:06:13 +00:00
Amin Ghadersohi 362b9faaab fix(mcp): preserve string metric keys and add geojson null filter in Deck.gl fallback
- Add _is_metric_ref() to distinguish saved metric keys (non-numeric strings
  like "count") from fixed display settings (numeric strings like "100");
  both size and metric fields are now validated with this helper so string
  metric references are no longer silently dropped
- Extend _deck_gl_null_filters() to add IS NOT NULL for the geojson column,
  mirroring the behavior of DeckGeoJson when filter_nulls is enabled
- Add unit tests for _is_metric_ref and the new integration paths
2026-05-27 20:09:08 +00:00
Amin Ghadersohi fdd97d42ad fix(mcp): guard Deck.gl metric extraction against scalar size and deck_geojson
_resolve_deck_gl_metrics now:
- Returns [] immediately for deck_geojson, matching DeckGeoJson.query_obj()
  which explicitly forces metrics=[] regardless of form_data
- Only includes size/metric values that are dicts (metric references);
  scalar values like "100" in deck_geojson/deck_path fixtures are fixed
  display settings and must not be passed as query metrics

Adds viz_type param and passes it from build_query_dicts_from_form_data.
Adds regression tests using scalar size fixtures.
2026-05-27 19:07:00 +00:00
Amin Ghadersohi 1d276e67b2 fix(mcp): include point_radius_fixed metrics, null filters, and tooltip columns in Deck.gl fallback
- Extract _resolve_deck_gl_metrics to handle point_radius_fixed when
  type=="metric" (deck_scatter radius, deck_polygon elevation)
- Add _deck_gl_null_filters to mirror BaseDeckGLViz.add_null_filters()
  behavior; applied by default when filter_nulls is not False
- Extend resolve_deck_gl_columns to include tooltip_contents column
  items and cross_filter_column, matching the normal Deck.gl query
- Add _deck_gl_tooltip_cols helper for tooltip column extraction
- Add unit tests covering all three behaviors
2026-05-27 17:08:06 +00:00
Amin Ghadersohi ed15be88d9 fix(mcp): fall back to form_data query for Deck.gl charts in get_chart_data
Deck.gl chart types (deck_scatter, deck_arc, deck_hex, etc.) use spatial
column configs (lat/lon pairs, geohash, delimited coordinates) rather than
the standard metrics/groupby structure. The get_chart_data MCP tool was
returning an early MissingQueryContext error for these charts when no
saved query_context was present, instead of falling back to form_data.

- Add _deck_gl_spatial_cols and resolve_deck_gl_columns helpers to
  chart_helpers.py to extract SQL column names from spatial control configs
  (latlong, delimited, geohash) plus line_column, geojson, dimension, and
  js_columns fields.
- Route deck_ viz types through the new helpers inside
  build_query_dicts_from_form_data, producing a raw-column or
  grouped-metric query matching BaseDeckGLViz.query_obj() semantics.
- Remove the early-exit error block for deck_ charts in get_chart_data.py;
  the existing safety-net (empty columns + empty metrics) still guards
  charts whose spatial config is completely absent.
- Add unit tests covering latlong, delimited, geohash, arc start/end,
  line_column, geojson, dimension, js_columns, deduplication, and the
  build_query_dicts_from_form_data Deck.gl branch.
2026-05-22 00:47:44 +00:00
d203f0de33 chore(sql-lab): finish SqlLab typed-dispatch migration for SaveDatasetModal (#40040)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 16:04:38 -07:00
a75f9b67b2 chore(superset-ui-switchboard): forward-compat fixes for TypeScript 6.0 (Phase E) (#40028)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:37:52 -07:00
3f0858e35d chore(sql-lab): migrate useDispatch to useAppDispatch (#40037)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 15:36:27 -07:00
Beto DealmeidaandGitHub 68c145adc3 feat(semantic layers): add metadata on additive metrics (#40279) 2026-05-20 18:29:28 -04:00
4a9aecda4a fix(dashboard-import): remap chartsInScope on import (#26338) (#40140)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-20 13:41:14 -07:00
46b2d7d7a9 test(dashboard-import): pin native filter scope rootPath preservation (#19944) (#40135)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 12:25:34 -07:00
f8600471fa test(datasets): regression test for Jinja not rendered on sync columns (#25839) (#40224)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-05-20 11:46:36 -07:00
105 changed files with 1542 additions and 516 deletions
-13
View File
@@ -120,19 +120,6 @@ repos:
entry: ruff check --fix --show-fixes
language: system
types: [python]
- id: ruff-import-placement
name: ruff (import placement / PLC0415)
# PLC0415 ("import should be at top-level") is preview-only in
# ruff, so we can't put it in `[tool.ruff.lint] select` without
# enabling preview mode globally (which would also activate
# behavior changes for unrelated stable rules). Run it as a
# dedicated step instead. Existing function-body imports are
# grandfathered with per-line `# noqa: PLC0415`; new code must
# either move the import to the top or add the same noqa with
# a justification.
entry: ruff check --select PLC0415 --preview --no-fix
language: system
types: [python]
- repo: local
hooks:
- id: pylint
+1 -34
View File
@@ -341,13 +341,6 @@ target-version = "py310"
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
#
# NOTE: PLC0415 (`import` should be at the top-level of a file) is enforced
# via a dedicated pre-commit hook (`ruff-import-placement`) rather than
# globally here, because the rule is still preview-only in ruff and
# enabling `preview = true` globally would also activate other preview-
# rule behavior changes we don't want. Existing function-body imports
# have been grandfathered with per-line `# noqa: PLC0415`.
select = [
"B904",
"E4",
@@ -397,37 +390,11 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
"superset/cli/update.py" = ["TID251"]
"superset/key_value/types.py" = ["TID251"]
"superset/translations/utils.py" = ["TID251"]
"superset/extensions/__init__.py" = ["TID251"]
"superset/utils/json.py" = ["TID251"]
"docker/*" = ["I"] # Docker config files have non-standard imports that vary by environment
"superset/db_engine_specs/lib.py" = ["E501"] # Database config file with long description strings
# PLC0415 — function-body imports. Allow in directories where this is a
# deliberate pattern rather than an oversight:
# - cli/, scripts/: subcommand-deferred imports (don't load heavy modules
# unless the subcommand actually runs).
# - tasks/: Celery task bodies routinely defer imports of the modules
# they orchestrate.
# - migrations/versions/: Alembic migrations import models at runtime
# to interact with the schema state, not at module load.
# - mcp_service/: MCP tools lazy-load resources on invocation so the
# server can register many tools without paying their import cost.
# - db_engine_specs/: engine specs defer driver imports so optional
# DB drivers don't have to be installed.
# - initialization/__init__.py, extensions/__init__.py: the app-factory
# and extension wiring are full of intentional circular-import
# workarounds.
"superset/cli/**/*.py" = ["PLC0415"]
"scripts/**/*.py" = ["PLC0415"]
"superset/tasks/**/*.py" = ["PLC0415"]
"superset/migrations/versions/**/*.py" = ["PLC0415"]
"superset/mcp_service/**/*.py" = ["PLC0415"]
"superset/db_engine_specs/**/*.py" = ["PLC0415"]
"superset/initialization/__init__.py" = ["PLC0415"]
"superset/extensions/__init__.py" = ["TID251", "PLC0415"]
"superset/app.py" = ["PLC0415"]
"tests/**/*.py" = ["PLC0415"] # Tests import fixtures lazily; rule still
# applies in CI to production code in src/.
[tool.ruff.lint.isort]
case-sensitive = false
combine-as-imports = true
@@ -92,6 +92,26 @@ class Dimension:
grain: Grain | None = None
class AggregationType(str, enum.Enum):
"""
Aggregation function applied by a metric.
Additivity (across an arbitrary set of grouping dimensions):
* ``SUM``, ``COUNT``: fully additive — sub-group sums roll up via ``sum``.
* ``MIN``, ``MAX``: roll up via ``min`` / ``max`` of sub-group values.
* ``AVG``, ``COUNT_DISTINCT``, ``OTHER``: not safely roll-uppable from
sub-aggregates without auxiliary data.
"""
SUM = "SUM"
COUNT = "COUNT"
MIN = "MIN"
MAX = "MAX"
AVG = "AVG"
COUNT_DISTINCT = "COUNT_DISTINCT"
OTHER = "OTHER"
@dataclass(frozen=True)
class Metric:
id: str
@@ -100,6 +120,7 @@ class Metric:
definition: str
description: str | None = None
aggregation: AggregationType | None = None
@dataclass(frozen=True)
+2 -2
View File
@@ -87,7 +87,7 @@ def extension_setup_for_dev():
"""Set up extension structure for dev testing."""
def _setup(base_path: Path) -> None:
import json # noqa: PLC0415
import json
# Create extension.json with new structure
extension_json = {
@@ -111,7 +111,7 @@ def extension_setup_for_bundling():
"""Set up a complete extension structure ready for bundling."""
def _setup(base_path: Path) -> None:
import json # noqa: PLC0415
import json
# Create dist directory with manifest and files
dist_dir = base_path / "dist"
@@ -347,7 +347,7 @@ def test_build_manifest_exits_when_extension_json_missing(isolated_filesystem):
@pytest.mark.unit
def test_clean_dist_frontend_removes_frontend_dist(isolated_filesystem):
"""Test clean_dist_frontend removes frontend/dist directory specifically."""
from superset_extensions_cli.cli import clean_dist_frontend # noqa: PLC0415
from superset_extensions_cli.cli import clean_dist_frontend
# Create dist/frontend structure
dist_dir = isolated_filesystem / "dist"
@@ -366,7 +366,7 @@ def test_clean_dist_frontend_removes_frontend_dist(isolated_filesystem):
@pytest.mark.unit
def test_clean_dist_frontend_handles_nonexistent_directory(isolated_filesystem):
"""Test clean_dist_frontend handles case where frontend dist doesn't exist."""
from superset_extensions_cli.cli import clean_dist_frontend # noqa: PLC0415
from superset_extensions_cli.cli import clean_dist_frontend
# No dist directory exists
clean_dist_frontend(isolated_filesystem)
@@ -377,7 +377,7 @@ def test_clean_dist_frontend_handles_nonexistent_directory(isolated_filesystem):
@pytest.mark.unit
def test_run_frontend_build_with_output_messages(isolated_filesystem):
"""Test run_frontend_build produces expected output messages."""
from superset_extensions_cli.cli import run_frontend_build # noqa: PLC0415
from superset_extensions_cli.cli import run_frontend_build
frontend_dir = isolated_filesystem / "frontend"
frontend_dir.mkdir()
@@ -406,7 +406,7 @@ def test_rebuild_frontend_handles_build_results(
isolated_filesystem, return_code, expected_result
):
"""Test rebuild_frontend handles different build results."""
from superset_extensions_cli.cli import rebuild_frontend # noqa: PLC0415
from superset_extensions_cli.cli import rebuild_frontend
# Create frontend structure
frontend_dir = isolated_filesystem / "frontend"
@@ -434,7 +434,7 @@ def test_rebuild_frontend_handles_build_results(
@pytest.mark.unit
def test_rebuild_backend_calls_copy_and_shows_message(isolated_filesystem):
"""Test rebuild_backend calls copy_backend_files and shows success message."""
from superset_extensions_cli.cli import rebuild_backend # noqa: PLC0415
from superset_extensions_cli.cli import rebuild_backend
# Create extension.json
extension_json = {
@@ -35,7 +35,7 @@ def test_validate_command_success(cli_runner, isolated_filesystem):
"version": "1.0.0",
"permissions": [],
}
import json # noqa: PLC0415
import json
(isolated_filesystem / "extension.json").write_text(json.dumps(extension_json))
@@ -181,7 +181,7 @@ def test_template_rendering_with_different_ids(
jinja_env, publisher, technical_name, display_name
):
"""Test templates render correctly with various publisher/name combinations."""
from superset_extensions_cli.utils import ( # noqa: PLC0415
from superset_extensions_cli.utils import (
get_module_federation_name,
kebab_to_snake_case,
)
@@ -95,8 +95,11 @@ class FakeMessageChannel {
const port2 = new FakeMessagePort();
port1.otherPort = port2;
port2.otherPort = port1;
this.port1 = port1;
this.port2 = port2;
// FakeMessagePort only implements the subset of MessagePort that
// Switchboard exercises; cast at the boundary so the fake satisfies
// the consumer signature without weakening the production type.
this.port1 = port1 as unknown as MessagePort;
this.port2 = port2 as unknown as MessagePort;
}
}
@@ -88,7 +88,7 @@ function isError(message: Message): message is ErrorMessage {
* Calling methods on the switchboard causes messages to be sent through the channel.
*/
export class Switchboard {
port: MessagePort;
port!: MessagePort;
name = '';
@@ -97,9 +97,9 @@ export class Switchboard {
// used to make unique ids
incrementor = 1;
debugMode: boolean;
debugMode = false;
private isInitialised: boolean;
private isInitialised = false;
constructor(params?: Params) {
if (!params) {
+12 -6
View File
@@ -1671,7 +1671,7 @@ export interface VizOptions {
export function createDatasource(
vizOptions: VizOptions,
): SqlLabThunkAction<Promise<unknown>> {
): SqlLabThunkAction<Promise<{ id: number }>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
const { dbId, catalog, schema, datasourceName, sql, templateParams } =
@@ -1691,9 +1691,10 @@ export function createDatasource(
}),
})
.then(({ json }) => {
dispatch(createDatasourceSuccess(json as { id: number }));
const result = json as { id: number };
dispatch(createDatasourceSuccess(result));
return Promise.resolve(json);
return result;
})
.catch(error => {
getClientErrorObject(error).then(e => {
@@ -1712,7 +1713,7 @@ export function createDatasource(
export function createCtasDatasource(
vizOptions: Record<string, unknown>,
): SqlLabThunkAction<Promise<{ id: number }>> {
): SqlLabThunkAction<Promise<{ table_id: number }>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
return SupersetClient.post({
@@ -1720,9 +1721,14 @@ export function createCtasDatasource(
jsonPayload: vizOptions,
})
.then(({ json }) => {
dispatch(createDatasourceSuccess(json.result));
const result = json.result as { table_id: number };
// The endpoint's `result.table_id` IS the dataset id; normalize so
// createDatasourceSuccess's `${data.id}__table` resolves correctly.
// Without this, the CTAS Explore button silently produced
// `"undefined__table"` because `result.id` doesn't exist.
dispatch(createDatasourceSuccess({ id: result.table_id }));
return json.result;
return result;
})
.catch(() => {
const errorMsg = t('An error occurred while creating the data source');
@@ -19,7 +19,8 @@
import { useRef, useEffect, FC, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { logging } from '@apache-superset/core/utils';
import {
SqlLabRootState,
@@ -86,7 +87,7 @@ const EditorAutoSync: FC = () => {
const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
state => state.sqlLab.editorTabLastUpdatedAt,
);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const lastSavedTimestampRef = useRef<number>(editorTabLastUpdatedAt);
const currentQueryEditorId = useSelector<SqlLabRootState, string>(
@@ -17,7 +17,8 @@
* under the License.
*/
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { usePrevious } from '@superset-ui/core';
import { css, useTheme } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
@@ -136,7 +137,7 @@ const EditorWrapper = ({
height,
hotkeys,
}: EditorWrapperProps) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'id',
'dbId',
@@ -17,7 +17,8 @@
* under the License.
*/
import { useEffect, useMemo, useRef } from 'react';
import { useDispatch, useStore } from 'react-redux';
import { useStore } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import { getExtensionsRegistry } from '@superset-ui/core';
@@ -68,7 +69,7 @@ export function useKeywords(
catalog,
schema,
});
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const hasFetchedKeywords = useRef(false);
// skipFetch is used to prevent re-evaluating memoized keywords
// due to updated api results by skip flag
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import { JsonObject, VizType } from '@superset-ui/core';
import { VizType } from '@superset-ui/core';
import {
createCtasDatasource,
addInfoToast,
@@ -45,7 +46,7 @@ const ExploreCtasResultsButton = ({
const errorMessage = useSelector(
(state: SqlLabRootState) => state.sqlLab.errorMessage,
);
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const dispatch = useAppDispatch();
const buildVizOptions = {
table_name: table,
@@ -56,7 +57,7 @@ const ExploreCtasResultsButton = ({
const visualize = () => {
dispatch(createCtasDatasource(buildVizOptions))
.then((data: { table_id: number }) => {
.then(data => {
const formData = {
datasource: `${data.table_id}__table`,
metrics: ['count'],
@@ -17,7 +17,8 @@
* under the License.
*/
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import URI from 'urijs';
import { pick } from 'lodash';
import { useComponentDidUpdate } from '@superset-ui/core';
@@ -49,7 +50,7 @@ const PopEditorTab: React.FC<{ children?: React.ReactNode }> = ({
({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0],
);
const [updatedUrl, setUpdatedUrl] = useState<string>(SQL_LAB_URL);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
useComponentDidUpdate(() => {
setQueryEditorId(assigned => assigned ?? activeQueryEditorId);
if (activeQueryEditorId) {
@@ -17,7 +17,8 @@
* under the License.
*/
import { useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { isObject } from 'lodash';
import rison from 'rison';
import {
@@ -82,7 +83,7 @@ function QueryAutoRefresh({
.map(({ id }) => id),
),
);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const checkForRefresh = () => {
const shouldRequestChecking = shouldCheckForQueries(queries);
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useDispatch } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import { Dropdown, Button } from '@superset-ui/core/components';
import { Menu } from '@superset-ui/core/components/Menu';
@@ -75,7 +75,7 @@ const QueryLimitSelect = ({
maxRow,
defaultQueryLimit,
}: QueryLimitSelectProps) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']);
const queryLimit = queryEditor.queryLimit || defaultQueryLimit;
@@ -30,7 +30,8 @@ import ProgressBar from '@superset-ui/core/components/ProgressBar';
import { t } from '@apache-superset/core/translation';
import { QueryResponse, QueryState } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/theme';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import {
queryEditorSetSql,
@@ -92,7 +93,7 @@ const QueryTable = ({
latestQueryId,
}: QueryTableProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const [selectedQuery, setSelectedQuery] = useState<QueryResponse | null>(
null,
);
@@ -27,7 +27,8 @@ import {
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { useHistory } from 'react-router-dom';
import { pick } from 'lodash';
import {
@@ -231,7 +232,7 @@ const ResultSet = ({
canCopyClipboardSqlLab: canCopyClipboard,
} = usePermissions();
const history = useHistory();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId });
const { showConfirm, ConfirmModal } = useConfirmModal();
@@ -16,8 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import * as reactRedux from 'react-redux';
import { act } from 'react';
import { act, type ComponentProps } from 'react';
import {
cleanup,
fireEvent,
@@ -40,6 +39,19 @@ const mockedProps = {
datasource: testQuery,
};
// Render with the SqlLab user fixture preloaded into the mock store so the
// component's useSelector(state => state.user) returns a useful value.
// Previously this test used jest.spyOn(reactRedux, 'useSelector') to inject
// the user directly, which can't intercept calls routed through the typed
// useAppSelector hook.
const renderModal = (
props: Partial<ComponentProps<typeof SaveDatasetModal>> = {},
) =>
render(<SaveDatasetModal {...mockedProps} {...props} />, {
useRedux: true,
initialState: { user },
});
fetchMock.get('glob:*/api/v1/dataset/?*', {
result: mockdatasets,
dataset_count: 3,
@@ -47,17 +59,17 @@ fetchMock.get('glob:*/api/v1/dataset/?*', {
jest.useFakeTimers({ advanceTimers: true });
// Mock the user
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
beforeEach(() => {
useSelectorMock.mockClear();
cleanup();
});
// Mock the createDatasource action
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
// Mock createDatasource to return a thunk that resolves with the dataset's
// new id. The test's mock store includes redux-thunk middleware (from RTK's
// getDefaultMiddleware), so dispatch(createDatasource(...)) properly unwraps
// the thunk and the production code's .then((data) => clearDatasetCache(data.id))
// chain receives `{ id: 123 }`. Individual tests can override per-call as needed.
jest.mock('src/SqlLab/actions/sqlLab', () => ({
createDatasource: jest.fn(),
createDatasource: jest.fn(() => () => Promise.resolve({ id: 123 })),
}));
jest.mock('src/explore/exploreUtils/formData', () => ({
postFormData: jest.fn(),
@@ -70,7 +82,7 @@ jest.mock('src/utils/cachedSupersetGet', () => ({
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetModal', () => {
test('renders a "Save as new" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const saveRadioBtn = screen.getByRole('radio', {
name: /save as new/i,
@@ -87,7 +99,7 @@ describe('SaveDatasetModal', () => {
});
test('renders an "Overwrite existing" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const overwriteRadioBtn = screen.getByRole('radio', {
name: /overwrite existing/i,
@@ -103,20 +115,20 @@ describe('SaveDatasetModal', () => {
});
test('renders a close button', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});
test('renders a save button when "Save as new" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// "Save as new" is selected when the modal opens by default
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
test('renders an overwrite button when "Overwrite existing" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -130,8 +142,7 @@ describe('SaveDatasetModal', () => {
});
test('renders the overwrite button as disabled until an existing dataset is selected', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -168,8 +179,7 @@ describe('SaveDatasetModal', () => {
});
test('renders a confirm overwrite screen when overwrite is clicked', async () => {
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
// Click the overwrite radio button
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -215,11 +225,7 @@ describe('SaveDatasetModal', () => {
});
test('sends the schema when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -240,17 +246,9 @@ describe('SaveDatasetModal', () => {
});
test('sends the catalog when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(
<SaveDatasetModal
{...mockedProps}
datasource={{ ...mockedProps.datasource, catalog: 'public' }}
/>,
{ useRedux: true },
);
renderModal({
datasource: { ...mockedProps.datasource, catalog: 'public' },
});
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -271,7 +269,7 @@ describe('SaveDatasetModal', () => {
});
test('does not renders a checkbox button when template processing is disabled', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
});
@@ -280,7 +278,7 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
@@ -289,15 +287,11 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
const propsWithTemplateParam = {
...mockedProps,
renderModal({
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -324,15 +318,11 @@ describe('SaveDatasetModal', () => {
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
};
const propsWithTemplateParam = {
...mockedProps,
renderModal({
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -393,19 +383,11 @@ describe('SaveDatasetModal', () => {
.spyOn(SupersetClient, 'put')
.mockResolvedValue({ json: { result: { id: 0 } } } as any);
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
const propsWithTemplateParam = {
...mockedProps,
renderModal({
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12, _filters: 'foo' }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
// Check the "Include Template Parameters" checkbox
@@ -443,19 +425,11 @@ describe('SaveDatasetModal', () => {
.spyOn(SupersetClient, 'put')
.mockResolvedValue({ json: { result: { id: 0 } } } as any);
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
const propsWithTemplateParam = {
...mockedProps,
renderModal({
datasource: {
...testQuery,
templateParams: JSON.stringify({ my_param: 12 }),
},
};
render(<SaveDatasetModal {...propsWithTemplateParam} />, {
useRedux: true,
});
// Do NOT check the "Include Template Parameters" checkbox
@@ -489,12 +463,9 @@ describe('SaveDatasetModal', () => {
'postFormData',
);
const dummyDispatch = jest.fn().mockResolvedValue({ id: 123 });
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
postFormData.mockResolvedValue('chart_key_123');
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
renderModal();
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -34,7 +34,6 @@ import { t } from '@apache-superset/core/translation';
import {
SupersetClient,
JsonResponse,
JsonObject,
QueryResponse,
QueryFormData,
VizType,
@@ -44,16 +43,14 @@ import {
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
import { useSelector, useDispatch } from 'react-redux';
import { useAppDispatch, useAppSelector } from 'src/views/store';
import rison from 'rison';
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import {
DatasetRadioState,
EXPLORE_CHART_DEFAULT,
DatasetOwner,
SqlLabRootState,
} from 'src/SqlLab/types';
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { postFormData } from 'src/explore/exploreUtils/formData';
@@ -221,7 +218,7 @@ export const SaveDatasetModal = ({
openWindow = true,
formData = {},
}: SaveDatasetModalProps) => {
const defaultVizType = useSelector<SqlLabRootState, string>(
const defaultVizType = useAppSelector(
state => state.common?.conf?.DEFAULT_VIZ_TYPE || VizType.Table,
);
@@ -240,8 +237,8 @@ export const SaveDatasetModal = ({
>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const user = useSelector<SqlLabRootState, User>(state => state.user);
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const user = useAppSelector(state => state.user);
const dispatch = useAppDispatch();
const [includeTemplateParameters, setIncludeTemplateParameters] =
useState(false);
@@ -17,7 +17,8 @@
* under the License.
*/
import { createRef, useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { nanoid } from 'nanoid';
import Tabs from '@superset-ui/core/components/Tabs';
import { t } from '@apache-superset/core/translation';
@@ -105,7 +106,7 @@ const SouthPane = ({
const { id, tabViewId } = useQueryEditor(queryEditorId, ['tabViewId']);
const editorId = tabViewId ?? id;
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const viewItems = views.getViews(ViewLocations.sqllab.panels) || [];
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
@@ -30,7 +30,8 @@ import {
import type { editors } from '@apache-superset/core';
import useEffectEvent from 'src/hooks/useEffectEvent';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import AutoSizer from 'react-virtualized-auto-sizer';
import { t } from '@apache-superset/core/translation';
import {
@@ -237,7 +238,7 @@ const SqlEditor: FC<Props> = ({
scheduleQueryWarning,
}) => {
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const { database, latestQuery, currentQueryEditorId, hasSqlStatement } =
useSelector<
@@ -17,7 +17,7 @@
* under the License.
*/
import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { resetState } from 'src/SqlLab/actions/sqlLab';
import {
@@ -69,7 +69,7 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } =
dbSelectorProps;
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const shouldShowReset = window.location.search === '?reset=1';
// Modal state for Database/Catalog/Schema selector
@@ -19,7 +19,8 @@
import { useMemo, FC } from 'react';
import { bindActionCreators } from 'redux';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { useSelector, shallowEqual } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { MenuDotsDropdown } from '@superset-ui/core/components';
import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
import { t } from '@apache-superset/core/translation';
@@ -90,7 +91,7 @@ const SqlEditorTabHeader: FC<Props> = ({ queryEditor }) => {
);
const StatusIcon = queryState ? STATE_ICONS[queryState] : STATE_ICONS.running;
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const actions = useMemo(
() =>
bindActionCreators(
@@ -17,7 +17,8 @@
* under the License.
*/
import { useEffect, useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { SqlLabRootState } from 'src/SqlLab/types';
import {
@@ -41,7 +42,7 @@ export default function useDatabaseSelector(queryEditorId: string) {
SqlLabRootState,
SqlLabRootState['sqlLab']['databases']
>(({ sqlLab }) => sqlLab.databases);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'dbId',
'catalog',
@@ -17,7 +17,8 @@
* under the License.
*/
import { useState, useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
import {
ButtonGroup,
@@ -75,7 +76,7 @@ const Fade = styled.div`
const TableElement = ({ table, ...props }: TableElementProps) => {
const { dbId, catalog, schema, name, expanded, id } = table;
const theme = useTheme();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const {
currentData: tableMetadata,
isSuccess: isMetadataSuccess,
@@ -25,7 +25,8 @@ import {
type ChangeEvent,
useMemo,
} from 'react';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { useSelector, shallowEqual } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { styled, css, useTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -163,7 +164,7 @@ const savePinnedSchemasToStorage = (
};
const TableExploreTree: React.FC<Props> = ({ queryEditorId }) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const theme = useTheme();
const treeRef = useRef<TreeApi<TreeNodeData>>(null);
const tables = useSelector(
@@ -17,7 +17,7 @@
* under the License.
*/
import { useMemo, useReducer, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { t } from '@apache-superset/core/translation';
import {
Table,
@@ -130,7 +130,7 @@ const useTreeData = ({
catalog,
pinnedTables,
}: UseTreeDataParams): UseTreeDataResult => {
const reduxDispatch = useDispatch();
const reduxDispatch = useAppDispatch();
// Schema data from API
const {
currentData: schemaData,
@@ -17,7 +17,8 @@
* under the License.
*/
import { type FC, useCallback, useMemo, useRef, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useSelector } from 'react-redux';
import { useAppDispatch } from 'src/views/store';
import { nanoid } from 'nanoid';
import { t } from '@apache-superset/core/translation';
import { ClientErrorObject, getExtensionsRegistry } from '@superset-ui/core';
@@ -110,7 +111,7 @@ const renderWell = (partitions: TableMetaData['partitions']) => {
};
const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const theme = useTheme();
const [databaseName, backend, disableDataPreview] = useSelector<
SqlLabRootState,
+11 -2
View File
@@ -22,12 +22,13 @@ import {
createListenerMiddleware,
StoreEnhancer,
} from '@reduxjs/toolkit';
import type { AnyAction } from 'redux';
import {
useDispatch,
useSelector,
type TypedUseSelectorHook,
} from 'react-redux';
import thunk from 'redux-thunk';
import thunk, { type ThunkDispatch } from 'redux-thunk';
import { api } from 'src/hooks/apiResources/queryApi';
import messageToastReducer from 'src/components/MessageToasts/reducers';
import charts from 'src/components/Chart/chartReducer';
@@ -188,6 +189,14 @@ export type RootState = ReturnType<typeof store.getState>;
// thunks resolve correctly), and `useAppSelector` infers `RootState` without
// callers having to annotate every selector. Required ahead of the
// react-redux v8+ bump, which tightens dispatch typing — see #39927.
export type AppDispatch = typeof store.dispatch;
//
// AppDispatch is declared as ThunkDispatch & store.dispatch rather than
// `typeof store.dispatch` because Superset annotates getMiddleware as
// ConfigureStoreOptions['middleware'], which erases the middleware tuple type
// and leaves store.dispatch typed as Dispatch<AnyAction>. The intersection
// restores thunk support without requiring a wider refactor of the middleware
// setup.
export type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction> &
typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+3 -3
View File
@@ -152,7 +152,7 @@ class AsyncQueryManager:
self.register_request_handlers(app)
# pylint: disable=import-outside-toplevel
from superset.tasks.async_queries import ( # noqa: PLC0415
from superset.tasks.async_queries import (
load_chart_data_into_cache,
load_explore_json_into_cache,
)
@@ -222,7 +222,7 @@ class AsyncQueryManager:
user_id: Optional[int] = None,
) -> dict[str, Any]:
# pylint: disable=import-outside-toplevel
from superset import security_manager # noqa: PLC0415
from superset import security_manager
job_metadata = self.init_job(channel_id, user_id)
self._load_explore_json_into_cache_job.delay(
@@ -242,7 +242,7 @@ class AsyncQueryManager:
user_id: Optional[int] = None,
) -> dict[str, Any]:
# pylint: disable=import-outside-toplevel
from superset import security_manager # noqa: PLC0415
from superset import security_manager
# if it's guest user, we want to pass the guest token to the celery task
# chart data cache key is calculated based on the current user
+1 -3
View File
@@ -1433,9 +1433,7 @@ class ChartDataQueryContextSchema(Schema):
def get_query_context_factory(self) -> QueryContextFactory:
if self.query_context_factory is None:
# pylint: disable=import-outside-toplevel
from superset.common.query_context_factory import ( # noqa: PLC0415
QueryContextFactory,
)
from superset.common.query_context_factory import QueryContextFactory
self.query_context_factory = QueryContextFactory()
return self.query_context_factory
+1 -1
View File
@@ -219,7 +219,7 @@ class ExportDashboardsCommand(ExportModelsCommand):
# Export related theme
if model.theme:
from superset.commands.theme.export import ExportThemesCommand # noqa: PLC0415
from superset.commands.theme.export import ExportThemesCommand
yield from ExportThemesCommand([model.theme.id]).run()
@@ -235,9 +235,9 @@ def export_dataset_data(
sample_rows: int | None = None,
) -> bytes | None:
"""Export dataset data to Parquet format. Returns bytes or None on failure."""
import pandas as pd # pylint: disable=import-outside-toplevel # noqa: PLC0415
import pandas as pd # pylint: disable=import-outside-toplevel
from superset import db # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset import db # pylint: disable=import-outside-toplevel
# Ensure dataset is attached to session and relationships are loaded
if dataset not in db.session:
@@ -394,7 +394,7 @@ def export_dashboard_yaml(
dataset_id_to_uuid: dict[int, str],
) -> dict[str, Any]:
"""Export dashboard to YAML format with proper ID remapping."""
from superset.utils import ( # noqa: PLC0415
from superset.utils import (
json as superset_json, # pylint: disable=import-outside-toplevel
)
@@ -62,6 +62,21 @@ def build_uuid_to_id_map(position: dict[str, Any]) -> dict[str, int]:
}
def _remap_charts_in_scope(container: dict[str, Any], id_map: dict[int, int]) -> None:
"""Remap source-env chart IDs in ``container["chartsInScope"]`` in place.
``chartsInScope`` is a denormalized cache of the charts a filter (native
or cross-filter) currently applies to. Both surfaces share this contract,
so they share this remap. Unresolvable IDs are dropped rather than
passed through, matching the convention used for ``scope.excluded``.
"""
charts_in_scope = container.get("chartsInScope")
if isinstance(charts_in_scope, list):
container["chartsInScope"] = [
id_map[old_id] for old_id in charts_in_scope if old_id in id_map
]
def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
config: dict[str, Any],
chart_ids: dict[str, int],
@@ -145,6 +160,8 @@ def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
_remap_charts_in_scope(native_filter, id_map)
# fix display control dataset references
for customization in (
fixed.get("metadata", {}).get("chart_customization_config") or []
@@ -170,7 +187,7 @@ def update_id_refs( # pylint: disable=too-many-locals # noqa: C901
return fixed
def update_cross_filter_scoping(
def update_cross_filter_scoping( # noqa: C901
config: dict[str, Any], id_map: dict[int, int]
) -> dict[str, Any]:
# fix cross filter references
@@ -185,6 +202,9 @@ def update_cross_filter_scoping(
id_map[old_id] for old_id in scope_excluded if old_id in id_map
]
# Global cross-filter chartsInScope mirrors the native-filter case.
_remap_charts_in_scope(cross_filter_global_config, id_map)
if "chart_configuration" in (metadata := fixed.get("metadata", {})):
# Build remapped configuration in a single pass for clarity/readability.
new_chart_configuration: dict[str, Any] = {}
@@ -212,6 +232,11 @@ def update_cross_filter_scoping(
if old_id in id_map
]
# Cross-filter chartsInScope mirrors the native-filter case.
cross_filters = chart_config.get("crossFilters")
if isinstance(cross_filters, dict):
_remap_charts_in_scope(cross_filters, id_map)
new_chart_configuration[str(new_id)] = chart_config
metadata["chart_configuration"] = new_chart_configuration
+2 -7
View File
@@ -158,13 +158,8 @@ class UpdateDatabaseCommand(BaseCommand):
"""
Update the catalog of the datasets that are associated with database.
"""
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.models.sql_lab import ( # noqa: PLC0415
Query,
SavedQuery,
TableSchema,
TabState,
)
from superset.connectors.sqla.models import SqlaTable
from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState
for model in [
SqlaTable,
@@ -101,7 +101,7 @@ class CSVReader(BaseDataReader):
return "c"
# Import pyarrow to verify it works properly
import pyarrow as pa # noqa: F401, PLC0415
import pyarrow as pa # noqa: F401
# Check if pandas has built-in pyarrow support
pandas_version = str(pd.__version__)
@@ -209,7 +209,7 @@ def load_data(data_uri: str, dataset: SqlaTable, database: Database) -> None:
:raises DatasetUnAllowedDataURI: If a dataset is trying
to load data from a URI that is not allowed.
"""
from superset.examples.helpers import normalize_example_data_url # noqa: PLC0415
from superset.examples.helpers import normalize_example_data_url
# Convert example URLs to align with configuration
data_uri = normalize_example_data_url(data_uri)
+2 -4
View File
@@ -216,9 +216,7 @@ def load_configs(
# Normalize example data URLs before schema validation
if prefix == "datasets" and "data" in config:
from superset.examples.helpers import ( # noqa: PLC0415
normalize_example_data_url,
)
from superset.examples.helpers import normalize_example_data_url
config["data"] = normalize_example_data_url(config["data"])
@@ -355,7 +353,7 @@ def safe_insert_dashboard_chart_relationships(
This function checks for existing relationships and only inserts new ones
to avoid duplicate key constraint errors.
"""
from sqlalchemy.sql import select # noqa: PLC0415
from sqlalchemy.sql import select
if not dashboard_chart_ids:
return
+1 -1
View File
@@ -179,7 +179,7 @@ class BaseReportState:
"""
Creates a Report execution log, uses the current computed last_value for Alerts
"""
from sqlalchemy.orm.exc import StaleDataError # noqa: PLC0415
from sqlalchemy.orm.exc import StaleDataError
try:
log = ReportExecutionLog(
+1 -1
View File
@@ -233,7 +233,7 @@ class BaseStreamingCSVExportCommand(BaseCommand):
)
except Exception as e:
logger.error("Error in streaming CSV generator: %s", e)
import traceback # noqa: PLC0415
import traceback
logger.error("Traceback: %s", traceback.format_exc())
+5 -5
View File
@@ -86,7 +86,7 @@ class CancelTaskCommand(BaseCommand):
:returns: The updated task model
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
# Lightweight fetch to compute dedup_key for locking
# This is needed to use the same lock key as SubmitTaskCommand
@@ -113,7 +113,7 @@ class CancelTaskCommand(BaseCommand):
# Publish abort notification AFTER transaction commits
# This prevents race conditions where listeners check DB before commit
if self._should_publish_abort:
from superset.tasks.manager import TaskManager # noqa: PLC0415
from superset.tasks.manager import TaskManager
TaskManager.publish_abort(self._task_uuid)
@@ -129,7 +129,7 @@ class CancelTaskCommand(BaseCommand):
:returns: The updated task model
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
# Check admin status (no DB access)
is_admin = security_manager.is_admin()
@@ -232,7 +232,7 @@ class CancelTaskCommand(BaseCommand):
:param is_admin: Whether current user is admin
:returns: The updated task model
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
try:
result: Task | None = TaskDAO.abort_task(
@@ -273,7 +273,7 @@ class CancelTaskCommand(BaseCommand):
:param user_id: ID of user to unsubscribe
:returns: The updated task model
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
self._action_taken = "unsubscribed"
+1 -1
View File
@@ -67,7 +67,7 @@ class TaskPruneCommand(BaseCommand):
# Select all IDs that need to be deleted
# Only delete completed tasks (success, failure, or aborted)
from superset.models.tasks import Task # noqa: PLC0415
from superset.models.tasks import Task
select_stmt = sa.select(Task.id).where(
Task.ended_at < datetime.now() - timedelta(days=self.retention_period_days),
+1 -1
View File
@@ -80,7 +80,7 @@ class SubmitTaskCommand(BaseCommand):
:returns: Tuple of (Task, is_new) where is_new is True if task was created
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
self.validate()
+2 -2
View File
@@ -97,7 +97,7 @@ class UpdateTaskCommand(BaseCommand):
:returns: The updated task model
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
self.validate()
@@ -129,7 +129,7 @@ class UpdateTaskCommand(BaseCommand):
:returns: The updated task model
"""
from superset.daos.tasks import TaskDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO
# Re-fetch model under lock to get fresh state
fresh_model = TaskDAO.find_one_or_none(
+2 -2
View File
@@ -68,7 +68,7 @@ class DeleteThemeCommand(BaseCommand):
def _dissociate_dashboards(self) -> None:
"""Dissociate dashboards from themes before deletion."""
from superset.models.dashboard import Dashboard # noqa: PLC0415
from superset.models.dashboard import Dashboard
theme_ids = [theme.id for theme in self._models or []]
if not theme_ids:
@@ -95,7 +95,7 @@ class DeleteThemeCommand(BaseCommand):
def _get_dashboard_usage(self) -> dict[int, list[str]]:
"""Get dashboard names that use these themes."""
from superset.models.dashboard import Dashboard # noqa: PLC0415
from superset.models.dashboard import Dashboard
theme_ids = [theme.id for theme in self._models or []]
if not theme_ids:
+3 -3
View File
@@ -34,9 +34,9 @@ logger = logging.getLogger(__name__)
def import_theme(config: dict[str, Any], overwrite: bool = False) -> "Theme | None":
"""Import a single theme from config dictionary"""
from superset import db, security_manager # noqa: PLC0415
from superset.models.core import Theme # noqa: PLC0415
from superset.utils.core import get_user # noqa: PLC0415
from superset import db, security_manager
from superset.models.core import Theme
from superset.utils.core import get_user
can_write = security_manager.can_access("can_write", "Theme")
existing = db.session.query(Theme).filter_by(uuid=config["uuid"]).first()
+1 -3
View File
@@ -457,9 +457,7 @@ class QueryContextProcessor:
annotation_layer: dict[str, Any], force: bool
) -> dict[str, Any]:
# pylint: disable=import-outside-toplevel
from superset.commands.chart.data.get_data_command import ( # noqa: PLC0415
ChartDataCommand,
)
from superset.commands.chart.data.get_data_command import ChartDataCommand
if not (chart := ChartDAO.find_by_id(annotation_layer["value"])):
raise QueryObjectValidationError(
+1 -1
View File
@@ -335,7 +335,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
return False
def _sanitize_filters(self) -> None:
from superset.jinja_context import get_template_processor # noqa: PLC0415
from superset.jinja_context import get_template_processor
needs_transpilation = self.extras.get("transpile_to_dialect", False)
+1 -1
View File
@@ -2053,7 +2053,7 @@ class SqlaTable(
:param query_obj: query object to analyze
:return: The extra cache keys
"""
from superset.utils.rls import collect_rls_predicates_for_sql # noqa: PLC0415
from superset.utils.rls import collect_rls_predicates_for_sql
extra_cache_keys = super().get_extra_cache_keys(query_obj)
if self.has_extra_cache_key_calls(query_obj):
+38 -45
View File
@@ -42,24 +42,22 @@ def inject_dao_implementations() -> None:
Replace abstract DAO classes in superset_core common/queries/tasks daos with
concrete implementations from Superset.
"""
import superset_core.common.daos as core_common_dao_module # noqa: PLC0415
import superset_core.queries.daos as core_queries_dao_module # noqa: PLC0415
import superset_core.tasks.daos as core_tasks_dao_module # noqa: PLC0415
import superset_core.common.daos as core_common_dao_module
import superset_core.queries.daos as core_queries_dao_module
import superset_core.tasks.daos as core_tasks_dao_module
from superset.daos.chart import ChartDAO as HostChartDAO # noqa: PLC0415
from superset.daos.dashboard import ( # noqa: PLC0415
DashboardDAO as HostDashboardDAO,
)
from superset.daos.database import DatabaseDAO as HostDatabaseDAO # noqa: PLC0415
from superset.daos.dataset import DatasetDAO as HostDatasetDAO # noqa: PLC0415
from superset.daos.key_value import KeyValueDAO as HostKeyValueDAO # noqa: PLC0415
from superset.daos.query import ( # noqa: PLC0415
from superset.daos.chart import ChartDAO as HostChartDAO
from superset.daos.dashboard import DashboardDAO as HostDashboardDAO
from superset.daos.database import DatabaseDAO as HostDatabaseDAO
from superset.daos.dataset import DatasetDAO as HostDatasetDAO
from superset.daos.key_value import KeyValueDAO as HostKeyValueDAO
from superset.daos.query import (
QueryDAO as HostQueryDAO,
SavedQueryDAO as HostSavedQueryDAO,
)
from superset.daos.tag import TagDAO as HostTagDAO # noqa: PLC0415
from superset.daos.tasks import TaskDAO as HostTaskDAO # noqa: PLC0415
from superset.daos.user import UserDAO as HostUserDAO # noqa: PLC0415
from superset.daos.tag import TagDAO as HostTagDAO
from superset.daos.tasks import TaskDAO as HostTaskDAO
from superset.daos.user import UserDAO as HostUserDAO
# Replace abstract classes in common.daos with concrete implementations
core_common_dao_module.DatasetDAO = HostDatasetDAO # type: ignore[assignment,misc]
@@ -85,24 +83,19 @@ def inject_model_implementations() -> None:
Uses in-place replacement to maintain single import location for extensions.
"""
import superset_core.common.models as core_common_models_module # noqa: PLC0415
import superset_core.queries.models as core_queries_models_module # noqa: PLC0415
import superset_core.tasks.models as core_tasks_models_module # noqa: PLC0415
from flask_appbuilder.security.sqla.models import User as HostUser # noqa: PLC0415
import superset_core.common.models as core_common_models_module
import superset_core.queries.models as core_queries_models_module
import superset_core.tasks.models as core_tasks_models_module
from flask_appbuilder.security.sqla.models import User as HostUser
from superset.connectors.sqla.models import ( # noqa: PLC0415
SqlaTable as HostDataset,
)
from superset.key_value.models import KeyValueEntry as HostKeyValue # noqa: PLC0415
from superset.models.core import Database as HostDatabase # noqa: PLC0415
from superset.models.dashboard import Dashboard as HostDashboard # noqa: PLC0415
from superset.models.slice import Slice as HostChart # noqa: PLC0415
from superset.models.sql_lab import ( # noqa: PLC0415
Query as HostQuery,
SavedQuery as HostSavedQuery,
)
from superset.models.tasks import Task as HostTask # noqa: PLC0415
from superset.tags.models import Tag as HostTag # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable as HostDataset
from superset.key_value.models import KeyValueEntry as HostKeyValue
from superset.models.core import Database as HostDatabase
from superset.models.dashboard import Dashboard as HostDashboard
from superset.models.slice import Slice as HostChart
from superset.models.sql_lab import Query as HostQuery, SavedQuery as HostSavedQuery
from superset.models.tasks import Task as HostTask
from superset.tags.models import Tag as HostTag
# In-place replacement in common.models
core_common_models_module.Database = HostDatabase # type: ignore[misc]
@@ -126,9 +119,9 @@ def inject_query_implementations() -> None:
Replace abstract query functions in superset_core.queries.query with concrete
implementations from Superset.
"""
import superset_core.queries.query as core_query_module # noqa: PLC0415
import superset_core.queries.query as core_query_module
from superset.sql.parse import SQLGLOT_DIALECTS # noqa: PLC0415
from superset.sql.parse import SQLGLOT_DIALECTS
def get_sqlglot_dialect(database: "Database") -> Any:
return (
@@ -144,12 +137,12 @@ def inject_task_implementations() -> None:
Replace abstract task functions in superset_core tasks.types and tasks.decorators
with concrete implementations from Superset.
"""
import superset_core.tasks.decorators as core_tasks_decorators_module # noqa: PLC0415
import superset_core.tasks.types as core_tasks_types_module # noqa: PLC0415
import superset_core.tasks.decorators as core_tasks_decorators_module
import superset_core.tasks.types as core_tasks_types_module
from superset.tasks.ambient_context import get_context # noqa: PLC0415
from superset.tasks.context import TaskContext # noqa: PLC0415
from superset.tasks.decorators import task # noqa: PLC0415
from superset.tasks.ambient_context import get_context
from superset.tasks.context import TaskContext
from superset.tasks.decorators import task
# Replace abstract classes and functions with concrete implementations
core_tasks_types_module.TaskContext = TaskContext # type: ignore[assignment,misc]
@@ -162,9 +155,9 @@ def inject_rest_api_implementations() -> None:
Replace abstract REST API decorators in superset_core.rest_api.decorators
with concrete implementations from Superset.
"""
import superset_core.rest_api.decorators as core_rest_api_module # noqa: PLC0415
import superset_core.rest_api.decorators as core_rest_api_module
from superset.extensions import appbuilder # noqa: PLC0415
from superset.extensions import appbuilder
T = TypeVar("T", bound=type["RestApi"])
@@ -226,10 +219,10 @@ def inject_model_session_implementation() -> None:
Replace abstract get_session function in superset_core.common.models with concrete
implementation from Superset.
"""
import superset_core.common.models as core_models_module # noqa: PLC0415
import superset_core.common.models as core_models_module
def get_session() -> scoped_session:
from superset import db # noqa: PLC0415
from superset import db
return db.session
@@ -242,10 +235,10 @@ def inject_semantic_layer_implementations() -> None:
superset_core.semantic_layers.decorators with a concrete implementation
that registers classes in the contributions registry.
"""
import superset_core.semantic_layers.decorators as core_sl_module # noqa: PLC0415
import superset_core.semantic_layers.decorators as core_sl_module
import superset.extensions.context as context_module # noqa: PLC0415
from superset.semantic_layers.registry import registry # noqa: PLC0415
import superset.extensions.context as context_module
from superset.semantic_layers.registry import registry
def semantic_layer_impl(
id: str,
+9 -9
View File
@@ -98,7 +98,7 @@ def create_tool_decorator(
def decorator(func: F) -> F:
try:
# Import here to avoid circular imports
from superset.mcp_service.app import mcp # noqa: PLC0415
from superset.mcp_service.app import mcp
# Use provided values or extract from function
base_tool_name = name or func.__name__
@@ -111,7 +111,7 @@ def create_tool_decorator(
# Store RBAC permission metadata on the function so
# mcp_auth_hook can read them at call time.
if class_permission_name:
from superset.mcp_service.auth import ( # noqa: PLC0415
from superset.mcp_service.auth import (
CLASS_PERMISSION_ATTR,
METHOD_PERMISSION_ATTR,
)
@@ -124,13 +124,13 @@ def create_tool_decorator(
# Conditionally apply authentication wrapper
if protect:
from superset.mcp_service.auth import mcp_auth_hook # noqa: PLC0415
from superset.mcp_service.auth import mcp_auth_hook
wrapped_func = mcp_auth_hook(func)
else:
wrapped_func = func
from fastmcp.tools import Tool # noqa: PLC0415
from fastmcp.tools import Tool
tool = Tool.from_function(
wrapped_func,
@@ -208,7 +208,7 @@ def create_prompt_decorator(
def decorator(func: F) -> F:
try:
# Import here to avoid circular imports
from superset.mcp_service.app import mcp # noqa: PLC0415
from superset.mcp_service.app import mcp
# Use provided values or extract from function
base_prompt_name = name or func.__name__
@@ -223,7 +223,7 @@ def create_prompt_decorator(
# Conditionally apply authentication wrapper
if protect:
from superset.mcp_service.auth import mcp_auth_hook # noqa: PLC0415
from superset.mcp_service.auth import mcp_auth_hook
wrapped_func = mcp_auth_hook(func)
else:
@@ -279,10 +279,10 @@ def initialize_core_mcp_dependencies() -> None:
Also imports MCP service app to register all host tools BEFORE extension loading.
"""
import superset_core.mcp.decorators # noqa: PLC0415
import superset_core.mcp.decorators
try:
from fastmcp.tools import Tool # noqa: F401, PLC0415
from fastmcp.tools import Tool # noqa: F401
except ImportError:
logger.info(
"fastmcp is not installed, skipping MCP initialization. "
@@ -299,7 +299,7 @@ def initialize_core_mcp_dependencies() -> None:
try:
# Import MCP service app to register host tools BEFORE extension loading
# This prevents host tools from being registered during extension context
from superset.mcp_service import app # noqa: F401, PLC0415
from superset.mcp_service import app # noqa: F401
logger.info("MCP service app imported - host tools registered")
except Exception as e:
+1 -3
View File
@@ -213,9 +213,7 @@ class TaskDAO(BaseDAO[Task]):
:returns: Task if aborted/aborting, None if not found or already finished
:raises TaskNotAbortableError: If in-progress task has no abort handler
"""
from superset.commands.tasks.exceptions import ( # noqa: PLC0415
TaskNotAbortableError,
)
from superset.commands.tasks.exceptions import TaskNotAbortableError
task = cls.find_one_or_none(skip_base_filter=skip_base_filter, uuid=task_uuid)
if not task:
+1 -3
View File
@@ -749,9 +749,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
# pylint: disable=import-outside-toplevel
from superset.datasets.datetime_format_detector import ( # noqa: PLC0415
DatetimeFormatDetector,
)
from superset.datasets.datetime_format_detector import DatetimeFormatDetector
try:
# Get force parameter from query string
+2 -6
View File
@@ -50,12 +50,8 @@ def DistributedLock( # noqa: N802
or Redis connection fails
"""
# pylint: disable=import-outside-toplevel
from superset.commands.distributed_lock.acquire import ( # noqa: PLC0415
AcquireDistributedLock,
)
from superset.commands.distributed_lock.release import ( # noqa: PLC0415
ReleaseDistributedLock,
)
from superset.commands.distributed_lock.acquire import AcquireDistributedLock
from superset.commands.distributed_lock.release import ReleaseDistributedLock
key = get_key(namespace, **kwargs)
+1 -1
View File
@@ -60,7 +60,7 @@ def get_dataset_config_from_yaml(example_dir: Path) -> Dict[str, Optional[str]]:
def get_examples_directory() -> Path:
"""Get the path to the examples directory."""
from .helpers import get_examples_folder # noqa: PLC0415
from .helpers import get_examples_folder
return Path(get_examples_folder())
+1 -1
View File
@@ -74,7 +74,7 @@ def load_parquet_table( # noqa: C901
Returns:
The created SqlaTable object
"""
from sqlalchemy import text # noqa: PLC0415
from sqlalchemy import text
if database is None:
database = get_example_database()
+2 -2
View File
@@ -104,7 +104,7 @@ def normalize_example_data_url(url: str) -> str:
Normalized file:// URL pointing to the Parquet file, or the original URL
if it's a remote URL (http://, https://, etc.)
"""
import os # noqa: PLC0415
import os
# Handle existing examples:// protocol
if url.startswith(EXAMPLES_PROTOCOL):
@@ -158,7 +158,7 @@ def read_example_data(
Returns:
DataFrame with the loaded data
"""
import os # noqa: PLC0415
import os
# Extract example name from filepath
if filepath.startswith(EXAMPLES_PROTOCOL):
+1 -1
View File
@@ -50,7 +50,7 @@ def _read_file_if_exists(base: Any, path: Any) -> str | None:
def _load_shared_configs(examples_root: Any) -> dict[str, str]:
"""Load shared database and metadata configs from _shared directory."""
from flask import current_app # noqa: PLC0415
from flask import current_app
contents: dict[str, str] = {}
base = files("superset")
+2 -2
View File
@@ -34,13 +34,13 @@ class ExtensionsRestApi(BaseApi):
def response(self, status_code: int, **kwargs: Any) -> Response:
"""Helper method to create JSON responses."""
from flask import jsonify # noqa: PLC0415
from flask import jsonify
return jsonify(kwargs), status_code
def response_404(self) -> Response:
"""Helper method to create 404 responses."""
from flask import jsonify # noqa: PLC0415
from flask import jsonify
return jsonify({"message": "Not found"}), 404
@@ -37,7 +37,7 @@ _watcher_lock = threading.Lock()
def _get_file_handler_class() -> Any:
"""Get the file handler class, importing watchdog only when needed."""
try:
from watchdog.events import FileSystemEventHandler # noqa: PLC0415
from watchdog.events import FileSystemEventHandler
class LocalExtensionFileHandler(FileSystemEventHandler):
"""Custom file system event handler for LOCAL_EXTENSIONS directories."""
@@ -131,7 +131,7 @@ def setup_local_extensions_watcher(app: Flask) -> None: # noqa: C901
return
try:
from watchdog.observers import Observer # noqa: PLC0415
from watchdog.observers import Observer
# Set up and start the file watcher
event_handler = handler_class()
+1 -1
View File
@@ -296,7 +296,7 @@ class SupersetShillelaghAdapter(Adapter):
This is done on initialization because it's expensive.
"""
# pylint: disable=import-outside-toplevel
from superset.models.core import Database # noqa: PLC0415
from superset.models.core import Database
database = (
db.session.query(Database).filter_by(database_name=self.database).first()
+4 -4
View File
@@ -79,7 +79,7 @@ class SupersetMetastoreCache(BaseCache):
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
# pylint: disable=import-outside-toplevel
from superset.daos.key_value import KeyValueDAO # noqa: PLC0415
from superset.daos.key_value import KeyValueDAO
KeyValueDAO.upsert_entry(
resource=RESOURCE,
@@ -93,7 +93,7 @@ class SupersetMetastoreCache(BaseCache):
def add(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
# pylint: disable=import-outside-toplevel
from superset.daos.key_value import KeyValueDAO # noqa: PLC0415
from superset.daos.key_value import KeyValueDAO
try:
KeyValueDAO.delete_expired_entries(RESOURCE)
@@ -112,7 +112,7 @@ class SupersetMetastoreCache(BaseCache):
def get(self, key: str) -> Any:
# pylint: disable=import-outside-toplevel
from superset.daos.key_value import KeyValueDAO # noqa: PLC0415
from superset.daos.key_value import KeyValueDAO
return KeyValueDAO.get_value(RESOURCE, self.get_key(key), self.codec)
@@ -125,6 +125,6 @@ class SupersetMetastoreCache(BaseCache):
@transaction()
def delete(self, key: str) -> Any:
# pylint: disable=import-outside-toplevel
from superset.daos.key_value import KeyValueDAO # noqa: PLC0415
from superset.daos.key_value import KeyValueDAO
return KeyValueDAO.delete_entry(RESOURCE, self.get_key(key))
+1 -1
View File
@@ -53,7 +53,7 @@ class SSHManager:
ssh_tunnel: "SSHTunnel",
sqlalchemy_database_uri: str,
) -> sshtunnel.SSHTunnelForwarder:
from superset.utils.ssh_tunnel import get_default_port # noqa: PLC0415
from superset.utils.ssh_tunnel import get_default_port
url = make_url_safe(sqlalchemy_database_uri)
backend = url.get_backend_name()
+1 -3
View File
@@ -276,9 +276,7 @@ def get_extensions() -> dict[str, LoadedExtension]:
# Load extensions from discovery path (.supx files)
if extensions_path := current_app.config.get("EXTENSIONS_PATH"):
from superset.extensions.discovery import ( # noqa: PLC0415
discover_and_load_extensions,
)
from superset.extensions.discovery import discover_and_load_extensions
for extension in discover_and_load_extensions(extensions_path):
extension_id = extension.manifest.id
+9 -9
View File
@@ -285,7 +285,7 @@ class ExtraCache:
"""
# pylint: disable=import-outside-toplevel
from superset.views.utils import get_form_data # noqa: PLC0415
from superset.views.utils import get_form_data
if has_request_context() and request.args.get(param):
return request.args.get(param, default)
@@ -407,7 +407,7 @@ class ExtraCache:
:return: returns a list of filters
"""
# pylint: disable=import-outside-toplevel
from superset.views.utils import get_form_data # noqa: PLC0415
from superset.views.utils import get_form_data
form_data, _ = get_form_data()
convert_legacy_filters_into_adhoc(form_data)
@@ -512,7 +512,7 @@ class ExtraCache:
:return: The corresponding time filter.
"""
# pylint: disable=import-outside-toplevel
from superset.views.utils import get_form_data # noqa: PLC0415
from superset.views.utils import get_form_data
form_data, _ = get_form_data()
convert_legacy_filters_into_adhoc(form_data)
@@ -948,7 +948,7 @@ class PrestoTemplateProcessor(JinjaTemplateProcessor):
"""
# pylint: disable=import-outside-toplevel
from superset.db_engine_specs.presto import PrestoEngineSpec # noqa: PLC0415
from superset.db_engine_specs.presto import PrestoEngineSpec
table_name, schema = self._schema_table(table_name, self._schema)
return cast(PrestoEngineSpec, self._database.db_engine_spec).latest_partition(
@@ -959,7 +959,7 @@ class PrestoTemplateProcessor(JinjaTemplateProcessor):
table_name, schema = self._schema_table(table_name, self._schema)
# pylint: disable=import-outside-toplevel
from superset.db_engine_specs.presto import PrestoEngineSpec # noqa: PLC0415
from superset.db_engine_specs.presto import PrestoEngineSpec
return cast(
PrestoEngineSpec, self._database.db_engine_spec
@@ -1052,7 +1052,7 @@ def dataset_macro(
the underlying dataset.
"""
# pylint: disable=import-outside-toplevel
from superset.daos.dataset import DatasetDAO # noqa: PLC0415
from superset.daos.dataset import DatasetDAO
dataset = DatasetDAO.find_by_id(dataset_id)
if not dataset:
@@ -1081,8 +1081,8 @@ def get_dataset_id_from_context(metric_key: str) -> int:
:returns: the dataset ID.
"""
# pylint: disable=import-outside-toplevel
from superset.daos.chart import ChartDAO # noqa: PLC0415
from superset.views.utils import loads_request_json # noqa: PLC0415
from superset.daos.chart import ChartDAO
from superset.views.utils import loads_request_json
form_data: dict[str, Any] = {}
exc_message = _(
@@ -1134,7 +1134,7 @@ def metric_macro(
:returns: the macro SQL syntax.
"""
# pylint: disable=import-outside-toplevel
from superset.daos.dataset import DatasetDAO # noqa: PLC0415
from superset.daos.dataset import DatasetDAO
if not dataset_id:
dataset_id = get_dataset_id_from_context(metric_key)
+153
View File
@@ -260,6 +260,140 @@ def merge_extra_form_data_filters_into_query(
merge_form_data_filters_into_query(query, extra_query_form_data)
def _deck_gl_spatial_cols(spatial: dict[str, Any] | None) -> list[str]:
"""Return the column names referenced by a single Deck.gl spatial control."""
if not isinstance(spatial, dict):
return []
spatial_type = spatial.get("type")
if spatial_type == "latlong":
return [c for c in [spatial.get("lonCol"), spatial.get("latCol")] if c]
if spatial_type == "delimited":
return [c for c in [spatial.get("lonlatCol")] if c]
if spatial_type == "geohash":
return [c for c in [spatial.get("geohashCol")] if c]
return []
def _deck_gl_tooltip_cols(tooltip_contents: list[Any] | None) -> list[str]:
"""Return column names from Deck.gl tooltip_contents config."""
cols: list[str] = []
for item in tooltip_contents or []:
if isinstance(item, str):
cols.append(item)
elif isinstance(item, dict) and item.get("item_type") == "column":
col = item.get("column_name")
if isinstance(col, str) and col:
cols.append(col)
return cols
def _is_metric_ref(value: Any) -> bool:
"""Return True if value is a metric reference (dict or non-numeric string).
Deck.gl size/metric fields hold either a dict metric definition or a
simple saved-metric string key (e.g. "count"). Scalar numeric strings
like "100" are fixed display settings and must not be treated as metrics.
"""
if isinstance(value, dict):
return True
if isinstance(value, str) and value:
try:
float(value)
return False
except ValueError:
return True
return False
def _deck_gl_null_filters(form_data: dict[str, Any]) -> list[dict[str, Any]]:
"""Build IS NOT NULL simple filters for Deck.gl spatial and data columns.
Mirrors BaseDeckGLViz.add_null_filters() behavior: spatial control columns,
line_column, and the geojson column are filtered for non-null values by
default.
"""
seen: set[str] = set()
result: list[dict[str, Any]] = []
for key in ("spatial", "start_spatial", "end_spatial"):
for col in _deck_gl_spatial_cols(form_data.get(key)):
if col not in seen:
seen.add(col)
result.append({"col": col, "op": "IS NOT NULL", "val": ""})
for field in ("line_column", "geojson"):
data_col = form_data.get(field)
if isinstance(data_col, str) and data_col and data_col not in seen:
seen.add(data_col)
result.append({"col": data_col, "op": "IS NOT NULL", "val": ""})
return result
def _resolve_deck_gl_metrics(
form_data: dict[str, Any], viz_type: str = ""
) -> list[Any]:
"""Extract metrics for Deck.gl chart types.
deck_geojson.query_obj() forces metrics=[] regardless of form_data.
For other types, size/metric values are included when they are metric
references (dicts or non-numeric strings); numeric scalars like "100"
are fixed display settings and are excluded.
deck_scatter and deck_polygon can additionally store metric-backed
values in point_radius_fixed (radius for scatter, elevation for polygon).
"""
if viz_type == "deck_geojson":
return []
metrics: list[Any] = []
for field in ("size", "metric"):
m = form_data.get(field)
if _is_metric_ref(m):
metrics.append(m)
prf = form_data.get("point_radius_fixed")
if isinstance(prf, dict) and prf.get("type") == "metric":
value = prf.get("value")
if value:
metrics.append(value)
elif _is_metric_ref(prf):
# Legacy deck_scatter: point_radius_fixed can be a bare metric key string
metrics.append(prf)
return metrics
def resolve_deck_gl_columns(form_data: dict[str, Any]) -> list[str]:
"""Extract SQL column names for Deck.gl chart types from form_data.
Deck.gl charts use spatial controls (lat/lon pairs, geohash, etc.)
rather than the standard metrics/groupby structure. This function
maps those spatial control configs to the actual column names
needed by the SQL query.
"""
seen: set[str] = set()
columns: list[str] = []
def _add(col: str | None) -> None:
if col and isinstance(col, str) and col not in seen:
seen.add(col)
columns.append(col)
# Most Deck.gl types use "spatial"; arc charts use start/end spatial
for key in ("spatial", "start_spatial", "end_spatial"):
for col in _deck_gl_spatial_cols(form_data.get(key)):
_add(col)
# deck_path / deck_polygon use a line column; deck_geojson uses geojson
for field in ("line_column", "geojson", "dimension"):
_add(form_data.get(field))
for col in form_data.get("js_columns") or []:
if isinstance(col, str):
_add(col)
for col in _deck_gl_tooltip_cols(form_data.get("tooltip_contents")):
_add(col)
_add(form_data.get("cross_filter_column"))
return columns
def resolve_metrics(form_data: dict[str, Any], viz_type: str) -> list[Any]:
"""Extract metrics from form_data, handling chart-type-specific fields."""
if viz_type == "bubble":
@@ -423,6 +557,25 @@ def build_query_dicts_from_form_data(
or (getattr(chart, "viz_type", "") if chart else "")
or ""
)
# Deck.gl charts use spatial column configs rather than the standard
# metrics / groupby fields. Extract columns from the spatial controls.
if viz_type.startswith("deck_"):
deck_columns = resolve_deck_gl_columns(form_data)
deck_metrics = _resolve_deck_gl_metrics(form_data, viz_type)
qd = _build_single_query_dict(
form_data,
deck_columns,
deck_metrics,
row_limit=row_limit,
order_desc=order_desc,
)
if form_data.get("filter_nulls", True):
null_filters = _deck_gl_null_filters(form_data)
if null_filters:
qd["filters"] = [*(qd.get("filters") or []), *null_filters]
return [qd]
is_timeseries = (
viz_type.startswith("echarts_timeseries") or viz_type == "mixed_timeseries"
)
@@ -340,31 +340,10 @@ async def get_chart_data( # noqa: C901
# groupby-like fields (entity, series, columns):
# world_map, treemap_v2, sunburst_v2, gauge_chart
# Bubble charts use x/y/size as separate metric fields.
# Deck.gl charts (deck_arc, deck_scatter, etc.) use spatial
# column configs (lat/lon, geohash, etc.) instead.
viz_type = chart.viz_type or ""
# Deck.gl chart types store spatial data (lat/lon)
# rather than traditional metrics/groupby. They
# require a saved query_context to retrieve data.
# Match by prefix to cover all current and future
# deck.gl viz types (deck_arc, deck_scatter, etc.).
if viz_type.startswith("deck_"):
await ctx.warning(
"Chart %s is a deck.gl visualization (%s) with no "
"saved query_context. Data retrieval requires "
"re-saving the chart in Superset." % (chart.id, viz_type)
)
return ChartError(
error=(
f"Chart {chart.id} is a deck.gl visualization "
f"(type: {viz_type}) with no saved query_context. "
f"Deck.gl charts use spatial data (lat/lon) that "
f"cannot be reconstructed from form_data alone. "
f"Please open this chart in Superset and re-save "
f"it to generate a query_context."
),
error_type="MissingQueryContext",
)
fallback_queries = build_query_dicts_from_form_data(
form_data,
chart.datasource_id,
+2 -2
View File
@@ -1317,7 +1317,7 @@ class Database(CoreDatabase, AuditMixinNullable, ImportExportMixin): # pylint:
:param options: QueryOptions with execution settings
:returns: QueryResult with status, data, and metadata
"""
from superset.sql.execution import SQLExecutor # noqa: PLC0415
from superset.sql.execution import SQLExecutor
return SQLExecutor(self).execute(sql, options)
@@ -1333,7 +1333,7 @@ class Database(CoreDatabase, AuditMixinNullable, ImportExportMixin): # pylint:
:param options: QueryOptions with execution settings
:returns: AsyncQueryHandle for tracking the query
"""
from superset.sql.execution import SQLExecutor # noqa: PLC0415
from superset.sql.execution import SQLExecutor
return SQLExecutor(self).execute_async(sql, options)
+2 -6
View File
@@ -1375,9 +1375,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
"""
# Import here to avoid circular dependency
# pylint: disable=import-outside-toplevel
from superset.common.utils.query_cache_manager import ( # noqa: PLC0415
QueryCacheManager,
)
from superset.common.utils.query_cache_manager import QueryCacheManager
# ensure query_object is immutable
query_object_clone = copy.copy(query_object)
@@ -2515,9 +2513,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
:return: Dict with validation result and any errors
"""
from superset.sql_validators.base import ( # noqa: PLC0415
SQLValidationAnnotation,
)
from superset.sql_validators.base import SQLValidationAnnotation
try:
# Process template
+2 -4
View File
@@ -342,9 +342,7 @@ class Slice( # pylint: disable=too-many-public-methods
def get_query_context_factory(self) -> QueryContextFactory:
if self.query_context_factory is None:
# pylint: disable=import-outside-toplevel
from superset.common.query_context_factory import ( # noqa: PLC0415
QueryContextFactory,
)
from superset.common.query_context_factory import QueryContextFactory
self.query_context_factory = QueryContextFactory()
return self.query_context_factory
@@ -365,7 +363,7 @@ def id_or_uuid_filter(id_or_uuid: str | int) -> BinaryExpression:
def set_related_perm(_mapper: Mapper, _connection: Connection, target: Slice) -> None:
# pylint: disable=import-outside-toplevel
from superset.daos.datasource import DatasourceDAO # noqa: PLC0415
from superset.daos.datasource import DatasourceDAO
src_class = DatasourceDAO.sources[target.datasource_type]
if id_ := target.datasource_id:
+1 -1
View File
@@ -238,7 +238,7 @@ class Query(
@property
def columns(self) -> list["TableColumn"]:
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
TableColumn,
)
+38 -41
View File
@@ -132,7 +132,7 @@ def _log_audit_event(action: str, payload: dict[str, Any]) -> None:
configured implementation (DBEventLogger, S3EventLogger, etc.)
receives these security audit events.
"""
from superset.extensions import ( # noqa: PLC0415
from superset.extensions import (
event_logger, # pylint: disable=import-outside-toplevel
)
@@ -654,7 +654,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
def request_loader(self, request: Request) -> Optional[User]:
# pylint: disable=import-outside-toplevel
from superset.extensions import feature_flag_manager # noqa: PLC0415
from superset.extensions import feature_flag_manager
if feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"):
return self.get_guest_user_from_request(request)
@@ -799,7 +799,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:param datasource: The datasource
:returns: Whether the user can access the datasource's schema
"""
from superset.connectors.sqla.models import BaseDatasource # noqa: PLC0415
from superset.connectors.sqla.models import BaseDatasource
# Admin/superuser override
if self.can_access_all_datasources():
@@ -847,7 +847,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
"""
Return True if an embedded user or DASHBOARD_RBAC user can drill a dataset.
"""
from superset import is_feature_enabled # noqa: PLC0415
from superset import is_feature_enabled
if (
(
@@ -909,7 +909,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:returns: Whether the user has drill access.
"""
from superset.models.slice import Slice # noqa: PLC0415
from superset.models.slice import Slice
# Drill to Detail: no slice/chart context, dataset must belong to the dashboard
if (
@@ -1097,7 +1097,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
user_datasources = set()
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
user_datasources.update(
self.session.query(SqlaTable)
@@ -1208,7 +1208,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
"""
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
default_catalog = database.get_default_catalog()
catalog = catalog or default_catalog
@@ -1274,7 +1274,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:returns: The set of accessible database catalogs
"""
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
if hierarchical and self.can_access_database(database):
return catalogs
@@ -1339,7 +1339,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:returns: The list of accessible SQL tables w/ schema
"""
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
if self.can_access_database(database):
return datasource_names
@@ -1428,8 +1428,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
"""
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.models import core as models # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
from superset.models import core as models
logger.info("Fetching a set of all perms to lookup which ones are missing")
all_pvs = {
@@ -1461,7 +1461,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
existing_pvs: set[tuple[str, str]],
) -> None:
"""Backfill perm columns and create missing PVMs for semantic models."""
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel
SemanticLayer,
SemanticView,
)
@@ -1959,10 +1959,10 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:param target: The database object
:return: A list of changed view menus (permission resource names)
""" # noqa: E501
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
SqlaTable,
)
from superset.models.slice import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.models.slice import ( # pylint: disable=import-outside-toplevel
Slice,
)
@@ -2039,7 +2039,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:param target: The changed dataset object
:return:
"""
from superset.models.core import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.models.core import ( # pylint: disable=import-outside-toplevel
Database,
)
@@ -2152,7 +2152,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:return:
"""
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
# Check if watched fields have changed
table = SqlaTable.__table__ # pylint: disable=no-member
@@ -2245,10 +2245,10 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:param target: Dataset that was updated
:return:
"""
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
SqlaTable,
)
from superset.models.slice import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.models.slice import ( # pylint: disable=import-outside-toplevel
Slice,
)
@@ -2318,10 +2318,10 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
old_permission_name,
new_permission_name,
)
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.connectors.sqla.models import ( # pylint: disable=import-outside-toplevel
SqlaTable,
)
from superset.models.slice import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.models.slice import ( # pylint: disable=import-outside-toplevel
Slice,
)
@@ -2379,7 +2379,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
Creates the datasource_access PVM and stores the perm string on the row.
"""
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel
SemanticLayer,
)
@@ -2406,7 +2406,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
Renames the FAB ViewMenu so the PVM stays in sync with the layer name.
Also cascades the rename to all semantic view perms under this layer.
"""
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel
SemanticLayer,
SemanticView,
)
@@ -2494,7 +2494,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
Looks up the layer name via connection since the ORM relationship may
not be loaded during event handling.
"""
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel
SemanticLayer,
SemanticView,
)
@@ -2526,7 +2526,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
Looks up the layer name via connection since the ORM relationship may
not be loaded during event handling.
"""
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.semantic_layers.models import ( # pylint: disable=import-outside-toplevel
SemanticLayer,
SemanticView,
)
@@ -2883,12 +2883,12 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
:raises SupersetSecurityException: If the user cannot access the resource
"""
# pylint: disable=import-outside-toplevel
from superset import is_feature_enabled # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.models.dashboard import Dashboard # noqa: PLC0415
from superset.models.slice import Slice # noqa: PLC0415
from superset.models.sql_lab import Query # noqa: PLC0415
from superset.utils.core import shortid # noqa: PLC0415
from superset import is_feature_enabled
from superset.connectors.sqla.models import SqlaTable
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query
from superset.utils.core import shortid
if sql and database:
query = Query(
@@ -2922,7 +2922,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
# If the DB engine spec doesn't implement the logic the schema is read
# from the SQLAlchemy URI if possible; if not, we use the SQLAlchemy
# inspector to read it.
from superset.models.sql_lab import Query # noqa: PLC0415
from superset.models.sql_lab import Query
default_schema = database.get_default_schema_for_query(
cast(Query, query),
@@ -3215,7 +3215,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
return cache[cache_key]
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import ( # noqa: PLC0415
from superset.connectors.sqla.models import (
RLSFilterRoles,
RLSFilterTables,
RowLevelSecurityFilter,
@@ -3299,7 +3299,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
return
# pylint: disable=import-outside-toplevel
from superset.connectors.sqla.models import ( # noqa: PLC0415
from superset.connectors.sqla.models import (
RLSFilterRoles,
RLSFilterTables,
RowLevelSecurityFilter,
@@ -3410,11 +3410,11 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
@staticmethod
def validate_guest_token_resources(resources: GuestTokenResources) -> None:
# pylint: disable=import-outside-toplevel
from superset.commands.dashboard.embedded.exceptions import ( # noqa: PLC0415
from superset.commands.dashboard.embedded.exceptions import (
EmbeddedDashboardNotFoundError,
)
from superset.daos.dashboard import EmbeddedDashboardDAO # noqa: PLC0415
from superset.models.dashboard import Dashboard # noqa: PLC0415
from superset.daos.dashboard import EmbeddedDashboardDAO
from superset.models.dashboard import Dashboard
for resource in resources:
if resource["type"] == GuestTokenResourceType.DASHBOARD.value:
@@ -3505,7 +3505,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
@staticmethod
def is_guest_user(user: Optional[Any] = None) -> bool:
# pylint: disable=import-outside-toplevel
from superset import is_feature_enabled # noqa: PLC0415
from superset import is_feature_enabled
if not is_feature_enabled("EMBEDDED_SUPERSET"):
return False
@@ -3598,10 +3598,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
# temporal change to remove the roles view from the security menu,
# after migrating all views to frontend, we will set FAB_ADD_SECURITY_VIEWS = False
def register_views(self) -> None:
from superset.views.auth import ( # noqa: PLC0415
SupersetAuthView,
SupersetRegisterUserView,
)
from superset.views.auth import SupersetAuthView, SupersetRegisterUserView
if self.register_superset_auth_view:
self.auth_view = self.appbuilder.add_view_no_menu(SupersetAuthView)
+1 -3
View File
@@ -36,9 +36,7 @@ def _sl(legacy: str, semantic: str) -> str:
# Imported lazily to avoid a circular import at module load time
# (superset.semantic_layers.labels is imported by superset.initialization,
# which is itself imported during superset package initialization).
from superset import ( # noqa: PLC0415
feature_flag_manager, # pylint: disable=import-outside-toplevel
)
from superset import feature_flag_manager # pylint: disable=import-outside-toplevel
return (
semantic
+9 -13
View File
@@ -150,7 +150,7 @@ class SemanticLayer(AuditMixinNullable, Model):
connection: Connection,
target: "SemanticLayer",
) -> None:
from superset import security_manager # noqa: PLC0415
from superset import security_manager
security_manager.semantic_layer_after_insert(mapper, connection, target)
@@ -160,7 +160,7 @@ class SemanticLayer(AuditMixinNullable, Model):
connection: Connection,
target: "SemanticLayer",
) -> None:
from superset import security_manager # noqa: PLC0415
from superset import security_manager
security_manager.semantic_layer_before_update(mapper, connection, target)
@@ -170,7 +170,7 @@ class SemanticLayer(AuditMixinNullable, Model):
connection: Connection,
target: "SemanticLayer",
) -> None:
from superset import security_manager # noqa: PLC0415
from superset import security_manager
security_manager.semantic_layer_after_delete(mapper, connection, target)
@@ -242,7 +242,7 @@ class SemanticView(AuditMixinNullable, Model):
connection: Connection,
target: "SemanticView",
) -> None:
from superset import security_manager # noqa: PLC0415
from superset import security_manager
security_manager.semantic_view_after_insert(mapper, connection, target)
@@ -252,7 +252,7 @@ class SemanticView(AuditMixinNullable, Model):
connection: Connection,
target: "SemanticView",
) -> None:
from superset import security_manager # noqa: PLC0415
from superset import security_manager
security_manager.semantic_view_before_update(mapper, connection, target)
@@ -262,7 +262,7 @@ class SemanticView(AuditMixinNullable, Model):
connection: Connection,
target: "SemanticView",
) -> None:
from superset import security_manager # noqa: PLC0415
from superset import security_manager
security_manager.semantic_view_after_delete(mapper, connection, target)
@@ -482,13 +482,9 @@ class SemanticView(AuditMixinNullable, Model):
def raise_for_access(self) -> None:
"""Check that the user has access to this semantic view."""
from superset import security_manager # noqa: PLC0415
from superset.errors import ( # noqa: PLC0415
ErrorLevel,
SupersetError,
SupersetErrorType,
)
from superset.exceptions import SupersetSecurityException # noqa: PLC0415
from superset import security_manager
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetSecurityException
if security_manager.can_access_all_datasources():
return
+3 -3
View File
@@ -109,7 +109,7 @@ def _handle_query_error(
def _serialize_payload(payload: dict[Any, Any]) -> bytes:
"""Serialize payload for storage based on RESULTS_BACKEND_USE_MSGPACK config."""
from superset import results_backend_use_msgpack # noqa: PLC0415
from superset import results_backend_use_msgpack
if results_backend_use_msgpack:
return msgpack.dumps(payload, default=json.json_iso_dttm_ser, use_bin_type=True)
@@ -298,8 +298,8 @@ def _serialize_result_set(
:param result_set: Query result set to serialize
:returns: Tuple of (serialized_data, columns)
"""
from superset import results_backend_use_msgpack # noqa: PLC0415
from superset.dataframe import df_to_records # noqa: PLC0415
from superset import results_backend_use_msgpack
from superset.dataframe import df_to_records
if results_backend_use_msgpack:
if has_app_context():
+29 -36
View File
@@ -122,7 +122,7 @@ def execute_sql_with_cursor(
:returns: List of (statement_sql, result_set, execution_time_ms, rowcount) tuples
Returns empty list if stopped. Raises exception on error (fail-fast).
"""
from superset.result_set import SupersetResultSet # noqa: PLC0415
from superset.result_set import SupersetResultSet
total = len(statements)
if total == 0:
@@ -214,7 +214,7 @@ class SQLExecutor:
See superset_core.api.models.Database.execute() for full documentation.
"""
from superset_core.queries.types import ( # noqa: PLC0415
from superset_core.queries.types import (
QueryOptions as QueryOptionsType,
QueryResult as QueryResultType,
QueryStatus,
@@ -341,7 +341,7 @@ class SQLExecutor:
See superset_core.api.models.Database.execute_async() for full documentation.
"""
from superset_core.queries.types import ( # noqa: PLC0415
from superset_core.queries.types import (
QueryOptions as QueryOptionsType,
QueryResult as QueryResultType,
QueryStatus,
@@ -363,7 +363,7 @@ class SQLExecutor:
# DRY RUN: Return transformed SQL as completed async handle
if opts.dry_run:
from superset_core.queries.types import StatementResult # noqa: PLC0415
from superset_core.queries.types import StatementResult
original_sqls = [stmt.format() for stmt in original_script.statements]
transformed_sqls = [stmt.format() for stmt in transformed_script.statements]
@@ -510,7 +510,7 @@ class SQLExecutor:
:param query: Query model for progress tracking
:returns: List of StatementResult objects
"""
from superset_core.queries.types import StatementResult # noqa: PLC0415
from superset_core.queries.types import StatementResult
# Get original statement strings
original_sqls = [stmt.format() for stmt in original_script.statements]
@@ -578,7 +578,7 @@ class SQLExecutor:
:param sql: SQL to log
:param schema: Schema name
"""
from superset import security_manager # noqa: PLC0415
from superset import security_manager
if log_query := app.config.get("QUERY_LOGGER"):
log_query(
@@ -607,9 +607,7 @@ class SQLExecutor:
statements before the failure
:returns: QueryResult with error status
"""
from superset_core.queries.types import ( # noqa: PLC0415
QueryResult as QueryResultType,
)
from superset_core.queries.types import QueryResult as QueryResultType
return QueryResultType(
status=status,
@@ -631,7 +629,7 @@ class SQLExecutor:
if template_params is None:
return sql
from superset.jinja_context import get_template_processor # noqa: PLC0415
from superset.jinja_context import get_template_processor
tp = get_template_processor(database=self.database)
return tp.process_template(sql, **template_params)
@@ -739,7 +737,7 @@ class SQLExecutor:
:param catalog: Catalog name
:param schema: Schema name
"""
from superset.utils.rls import apply_rls # noqa: PLC0415
from superset.utils.rls import apply_rls
# Apply RLS to each statement in the script
for statement in script.statements:
@@ -763,7 +761,7 @@ class SQLExecutor:
:param status: Initial QueryStatus (RUNNING for sync, PENDING for async)
:returns: Query model instance
"""
from superset.models.sql_lab import Query as QueryModel # noqa: PLC0415
from superset.models.sql_lab import Query as QueryModel
user_id = None
if has_app_context() and hasattr(g, "user") and g.user:
@@ -795,7 +793,7 @@ class SQLExecutor:
:param opts: Query options
:returns: Cached QueryResult if found, None otherwise
"""
from superset_core.queries.types import ( # noqa: PLC0415
from superset_core.queries.types import (
QueryResult as QueryResultType,
QueryStatus,
StatementResult,
@@ -835,7 +833,7 @@ class SQLExecutor:
:param sql: SQL query (for cache key)
:param opts: Query options
"""
from superset_core.queries.types import QueryStatus # noqa: PLC0415
from superset_core.queries.types import QueryStatus
if result.status != QueryStatus.SUCCESS:
return
@@ -851,7 +849,7 @@ class SQLExecutor:
# Convert DataFrames to list-of-dicts so the cache backend
# does not need to pickle pandas objects (which can fail to
# deserialize correctly with some backends or pandas versions).
import pandas as pd # noqa: PLC0415
import pandas as pd
cached_data = {
"statements": [
@@ -908,9 +906,9 @@ class SQLExecutor:
:param rendered_sql: Rendered SQL to execute
:raises: Re-raises any exception after marking query as failed
"""
from superset.sql.execution.celery_task import execute_sql_task # noqa: PLC0415
from superset.utils.core import get_username # noqa: PLC0415
from superset.utils.dates import now_as_float # noqa: PLC0415
from superset.sql.execution.celery_task import execute_sql_task
from superset.utils.core import get_username
from superset.utils.dates import now_as_float
try:
task = execute_sql_task.delay(
@@ -933,7 +931,7 @@ class SQLExecutor:
:param query_id: ID of the Query model
:returns: AsyncQueryHandle with configured methods
"""
from superset_core.queries.types import ( # noqa: PLC0415
from superset_core.queries.types import (
AsyncQueryHandle as AsyncQueryHandleType,
QueryResult as QueryResultType,
QueryStatus,
@@ -972,7 +970,7 @@ class SQLExecutor:
:param cached_result: The cached QueryResult
:returns: AsyncQueryHandle that returns the cached data
"""
from superset_core.queries.types import ( # noqa: PLC0415
from superset_core.queries.types import (
AsyncQueryHandle as AsyncQueryHandleType,
QueryResult as QueryResultType,
QueryStatus,
@@ -1003,11 +1001,9 @@ class SQLExecutor:
@staticmethod
def _get_async_query_status(query_id: int) -> Any:
"""Get the current status of an async query."""
from superset_core.queries.types import ( # noqa: PLC0415
QueryStatus as QueryStatusType,
)
from superset_core.queries.types import QueryStatus as QueryStatusType
from superset.models.sql_lab import Query as QueryModel # noqa: PLC0415
from superset.models.sql_lab import Query as QueryModel
query = db.session.query(QueryModel).filter_by(id=query_id).one_or_none()
if not query:
@@ -1026,14 +1022,14 @@ class SQLExecutor:
@staticmethod
def _get_async_query_result(query_id: int) -> Any:
"""Get the result of an async query."""
import pandas as pd # noqa: PLC0415
from superset_core.queries.types import ( # noqa: PLC0415
import pandas as pd
from superset_core.queries.types import (
QueryResult as QueryResultType,
QueryStatus as QueryStatusType,
StatementResult,
)
from superset.models.sql_lab import Query as QueryModel # noqa: PLC0415
from superset.models.sql_lab import Query as QueryModel
query = db.session.query(QueryModel).filter_by(id=query_id).one_or_none()
if not query:
@@ -1052,16 +1048,16 @@ class SQLExecutor:
# Fetch results from results backend
if query.results_key:
import msgpack # noqa: PLC0415
import msgpack
from superset import results_backend_manager # noqa: PLC0415
from superset import results_backend_manager
results_backend = results_backend_manager.results_backend
if results_backend is not None:
blob = results_backend.get(query.results_key)
if blob:
try:
from superset.utils.core import zlib_decompress # noqa: PLC0415
from superset.utils.core import zlib_decompress
payload = msgpack.loads(zlib_decompress(blob))
@@ -1111,7 +1107,7 @@ class SQLExecutor:
@staticmethod
def _cancel_async_query(query_id: int, database: Database) -> bool:
"""Cancel an async query."""
from superset.models.sql_lab import Query as QueryModel # noqa: PLC0415
from superset.models.sql_lab import Query as QueryModel
query = db.session.query(QueryModel).filter_by(id=query_id).one_or_none()
if not query:
@@ -1132,11 +1128,8 @@ class SQLExecutor:
:param query: Query model instance to cancel
:returns: True if cancelled successfully, False otherwise
"""
from superset.constants import ( # noqa: PLC0415
QUERY_CANCEL_KEY,
QUERY_EARLY_CANCEL_KEY,
)
from superset.utils.core import QuerySource # noqa: PLC0415
from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY
from superset.utils.core import QuerySource
# Some engines implicitly handle cancellation
if database.db_engine_spec.has_implicit_cancel():
+2 -2
View File
@@ -1550,7 +1550,7 @@ def process_jinja_sql(
:raises jinja2.exceptions.TemplateError: If the Jinjafied SQL could not be rendered
"""
from superset.jinja_context import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from superset.jinja_context import ( # pylint: disable=import-outside-toplevel
get_template_processor,
)
@@ -1605,7 +1605,7 @@ def sanitize_clause(clause: str, engine: str) -> str:
try:
statement = SQLStatement(clause, engine)
dialect = SQLGLOT_DIALECTS.get(engine)
from sqlglot.dialects.dialect import Dialect # noqa: PLC0415
from sqlglot.dialects.dialect import Dialect
return Dialect.get_or_raise(dialect).generate(
statement._parsed, # pylint: disable=protected-access
+1 -1
View File
@@ -63,7 +63,7 @@ class PrestoDBSQLValidator(BaseSQLValidator):
# these EXPLAIN queries done in validation as proper Query objects
# in the superset ORM.
# pylint: disable=import-outside-toplevel
from pyhive.exc import DatabaseError # noqa: PLC0415
from pyhive.exc import DatabaseError
try:
db_engine_spec.execute(cursor, sql, database)
+2 -4
View File
@@ -67,9 +67,7 @@ class SqlQueryRenderImpl(SqlQueryRender):
self._raise_template_exception(ex, execution_context)
return "NOT_REACHABLE_CODE"
except Exception as ex:
from superset.jinja_context import ( # noqa: PLC0415
UndefinedTemplateFunctionException,
)
from superset.jinja_context import UndefinedTemplateFunctionException
if isinstance(ex, UndefinedTemplateFunctionException):
return query_model.sql.strip().strip(";")
@@ -80,7 +78,7 @@ class SqlQueryRenderImpl(SqlQueryRender):
execution_context: SqlJsonExecutionContext,
sql: str,
) -> str:
from superset.sql.parse import SQLScript # noqa: PLC0415
from superset.sql.parse import SQLScript
engine = execution_context.query.database.db_engine_spec.engine
script = SQLScript(sql, engine)
+14 -14
View File
@@ -18,14 +18,14 @@
def register_sqla_event_listeners() -> None:
import sqlalchemy as sqla # noqa: PLC0415
import sqlalchemy as sqla
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.models.core import FavStar # noqa: PLC0415
from superset.models.dashboard import Dashboard # noqa: PLC0415
from superset.models.slice import Slice # noqa: PLC0415
from superset.models.sql_lab import SavedQuery # noqa: PLC0415
from superset.tags.models import ( # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.tags.models import (
ChartUpdater,
DashboardUpdater,
DatasetUpdater,
@@ -54,14 +54,14 @@ def register_sqla_event_listeners() -> None:
def clear_sqla_event_listeners() -> None:
import sqlalchemy as sqla # noqa: PLC0415
import sqlalchemy as sqla
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.models.core import FavStar # noqa: PLC0415
from superset.models.dashboard import Dashboard # noqa: PLC0415
from superset.models.slice import Slice # noqa: PLC0415
from superset.models.sql_lab import SavedQuery # noqa: PLC0415
from superset.tags.models import ( # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.tags.models import (
ChartUpdater,
DashboardUpdater,
DatasetUpdater,
+5 -5
View File
@@ -421,7 +421,7 @@ class ThemeRestApi(BaseSupersetModelRestApi):
is_system=False, # User-created themes are never system themes
)
from superset.extensions import db # noqa: PLC0415
from superset.extensions import db
db.session.add(new_theme)
db.session.flush() # Flush to get the ID
@@ -601,7 +601,7 @@ class ThemeRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
# Check if user is admin
from superset import security_manager # noqa: PLC0415
from superset import security_manager
if not security_manager.is_admin():
return self.response(
@@ -668,7 +668,7 @@ class ThemeRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
# Check if user is admin
from superset import security_manager # noqa: PLC0415
from superset import security_manager
if not security_manager.is_admin():
return self.response(
@@ -721,7 +721,7 @@ class ThemeRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
# Check if user is admin
from superset import security_manager # noqa: PLC0415
from superset import security_manager
if not security_manager.is_admin():
return self.response(
@@ -771,7 +771,7 @@ class ThemeRestApi(BaseSupersetModelRestApi):
$ref: '#/components/responses/500'
"""
# Check if user is admin
from superset import security_manager # noqa: PLC0415
from superset import security_manager
if not security_manager.is_admin():
return self.response(
+1 -1
View File
@@ -241,7 +241,7 @@ class CacheManager:
def _init_distributed_coordination(self, app: Flask) -> None:
"""Initialize the distributed coordination backend (pub/sub, locks, streams)."""
from superset.async_events.cache_backend import ( # noqa: PLC0415
from superset.async_events.cache_backend import (
RedisCacheBackend,
RedisSentinelCacheBackend,
)
+1 -1
View File
@@ -595,7 +595,7 @@ def sanitize_url(url: str) -> str:
return url
try:
from urllib.parse import urlparse # noqa: PLC0415
from urllib.parse import urlparse
parsed = urlparse(url)
+3 -3
View File
@@ -36,8 +36,8 @@ def get_or_create_db(
database_name: str, sqlalchemy_uri: str, always_create: bool | None = True
) -> Database:
# pylint: disable=import-outside-toplevel
from superset import db # noqa: PLC0415
from superset.models import core as models # noqa: PLC0415
from superset import db
from superset.models import core as models
database = (
db.session.query(models.Database).filter_by(database_name=database_name).first()
@@ -81,7 +81,7 @@ def get_main_database() -> Database:
# with above function... think of how to refactor it
def remove_database(database: Database) -> None:
# pylint: disable=import-outside-toplevel
from superset import db # noqa: PLC0415
from superset import db
db.session.delete(database)
db.session.flush()
+1 -1
View File
@@ -611,7 +611,7 @@ def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-m
# that is made available in some plugins behind the experimental
# feature flag.
# pylint: disable=import-outside-toplevel
from superset import feature_flag_manager # noqa: PLC0415
from superset import feature_flag_manager
if feature_flag_manager.is_feature_enabled("CHART_PLUGINS_EXPERIMENTAL"):
time_unit = ""
+1 -3
View File
@@ -249,9 +249,7 @@ def transaction( # pylint: disable=redefined-outer-name
def decorate(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapped(*args: Any, **kwargs: Any) -> Any:
from superset import ( # noqa: PLC0415
db, # pylint: disable=import-outside-toplevel
)
from superset import db # pylint: disable=import-outside-toplevel
if getattr(g, "in_transaction", False):
# If already in a transaction, call the function directly
+2 -4
View File
@@ -102,9 +102,7 @@ class EncryptedFieldFactory:
class SecretsMigrator:
def __init__(self, previous_secret_key: str) -> None:
from superset import ( # noqa: PLC0415
db, # pylint: disable=import-outside-toplevel
)
from superset import db # pylint: disable=import-outside-toplevel
self._db = db
self._previous_secret_key = previous_secret_key
@@ -131,7 +129,7 @@ class SecretsMigrator:
:return: mapping of table name to (Table, {column name: EncryptedType})
"""
from flask_appbuilder import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
from flask_appbuilder import ( # pylint: disable=import-outside-toplevel
Model as FABModel,
)
+2 -2
View File
@@ -26,8 +26,8 @@ def get_dataset_access_filters(
*args: Any,
) -> BooleanClauseList:
# pylint: disable=import-outside-toplevel
from superset import security_manager # noqa: PLC0415
from superset.connectors.sqla.models import Database # noqa: PLC0415
from superset import security_manager
from superset.connectors.sqla.models import Database
database_ids = security_manager.get_accessible_databases()
perms = security_manager.user_view_menu_names("datasource_access")
+4 -4
View File
@@ -178,8 +178,8 @@ class AbstractEventLogger(ABC):
**payload_override: dict[str, Any] | None,
) -> None:
# pylint: disable=import-outside-toplevel
from superset import db # noqa: PLC0415
from superset.views.core import get_form_data # noqa: PLC0415
from superset import db
from superset.views.core import get_form_data
referrer = request.referrer[:1000] if request and request.referrer else None
@@ -380,8 +380,8 @@ class DBEventLogger(AbstractEventLogger):
**kwargs: Any,
) -> None:
# pylint: disable=import-outside-toplevel
from superset import db # noqa: PLC0415
from superset.models.core import Log # noqa: PLC0415
from superset import db
from superset.models.core import Log
records = kwargs.get("records", [])
curated_payload = kwargs.get("curated_payload")
+1 -1
View File
@@ -180,7 +180,7 @@ def add_data(
:param bool append: if the table already exists, append data or replace?
"""
# pylint: disable=import-outside-toplevel
from superset.utils.database import get_example_database # noqa: PLC0415
from superset.utils.database import get_example_database
database = get_example_database()
table_exists = database.has_table(Table(table_name))
+2 -2
View File
@@ -102,7 +102,7 @@ def get_oauth2_access_token(
a valid token when they retry.
""" # noqa: E501
# pylint: disable=import-outside-toplevel
from superset.models.core import DatabaseUserOAuth2Tokens # noqa: PLC0415
from superset.models.core import DatabaseUserOAuth2Tokens
token = (
db.session.query(DatabaseUserOAuth2Tokens)
@@ -131,7 +131,7 @@ def refresh_oauth2_token(
db_engine_spec: type[BaseEngineSpec],
) -> str | None:
# pylint: disable=import-outside-toplevel
from superset.models.core import DatabaseUserOAuth2Tokens # noqa: PLC0415
from superset.models.core import DatabaseUserOAuth2Tokens
# Use longer TTL for OAuth2 token refresh (may involve network calls)
with DistributedLock(
@@ -56,7 +56,7 @@ def _prophet_fit_and_predict( # pylint: disable=too-many-arguments
# `prophet` complains about `plotly` not being installed
with suppress_logging("prophet.plot"):
# pylint: disable=import-outside-toplevel
from prophet import Prophet # noqa: PLC0415
from prophet import Prophet
prophet_logger = logging.getLogger("prophet.plot")
prophet_logger.setLevel(logging.CRITICAL)
+2 -2
View File
@@ -84,7 +84,7 @@ def get_predicates_for_table(
table must be fully qualified, with catalog (null if the DB doesn't support) and
schema.
"""
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
# if the dataset in the RLS has null catalog, match it when using the default
# catalog
@@ -158,7 +158,7 @@ def collect_rls_predicates_for_sql(
(kept consistent with what's actually applied at query time).
:return: List of RLS predicate strings that would be applied
"""
from superset.sql.parse import SQLScript # noqa: PLC0415
from superset.sql.parse import SQLScript
try:
parsed_script = SQLScript(sql, engine=database.db_engine_spec.engine)
+1 -1
View File
@@ -125,7 +125,7 @@ def validate_webdriver_config() -> dict[str, Any]:
Returns a dictionary with the status of available webdrivers
and feature flags.
"""
from superset import feature_flag_manager # noqa: PLC0415
from superset import feature_flag_manager
return {
"selenium_available": True, # Always available as required dependency
+1 -3
View File
@@ -129,9 +129,7 @@ class Api(BaseSupersetView):
def get_query_context_factory(self) -> QueryContextFactory:
if self.query_context_factory is None:
# pylint: disable=import-outside-toplevel
from superset.common.query_context_factory import ( # noqa: PLC0415
QueryContextFactory,
)
from superset.common.query_context_factory import QueryContextFactory
self.query_context_factory = QueryContextFactory()
return self.query_context_factory
+2 -5
View File
@@ -214,12 +214,9 @@ class Superset(BaseSupersetView):
@staticmethod
def _generate_xlsx(viz_obj: BaseViz) -> FlaskResponse:
import pandas as pd # noqa: PLC0415
import pandas as pd
from superset.utils.excel import ( # noqa: PLC0415
apply_column_types,
df_to_excel,
)
from superset.utils.excel import apply_column_types, df_to_excel
payload = viz_obj.get_df_payload()
df = payload.get("df")
+1 -1
View File
@@ -117,7 +117,7 @@ class FilterRelatedTables(BaseFilter): # pylint: disable=too-few-public-methods
arg_name = "tables"
def apply(self, query: Query, value: Optional[Any]) -> Query:
from superset.connectors.sqla.models import SqlaTable # noqa: PLC0415
from superset.connectors.sqla.models import SqlaTable
like_value = "%" + cast(str, value) + "%"
return query.filter(SqlaTable.table_name.ilike(like_value))
+1 -1
View File
@@ -40,6 +40,6 @@ def version() -> FlaskResponse:
Return comprehensive version information including Git SHA
and branch when available.
"""
from superset.utils.version import get_version_metadata # noqa: PLC0415
from superset.utils.version import get_version_metadata
return jsonify(get_version_metadata())

Some files were not shown because too many files have changed in this diff Show More