This is an automated email from the ASF dual-hosted git repository.
craigrueda pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 515452c7e2 chore(async): Making create app configurable (#25346)
515452c7e2 is described below
commit 515452c7e2fb5da58bda788f4cf19e854e814852
Author: Craig Rueda <[email protected]>
AuthorDate: Wed Sep 20 10:04:58 2023 -0700
chore(async): Making create app configurable (#25346)
---
.gitignore | 1 +
docker-compose.yml | 8 ++
docker/nginx/nginx.conf | 127 +++++++++++++++++++++
superset/app.py | 7 +-
superset/async_events/api.py | 2 +-
.../{utils => async_events}/async_query_manager.py | 0
.../async_events/async_query_manager_factory.py | 27 +++--
superset/charts/data/api.py | 2 +-
superset/config.py | 3 +
superset/extensions/__init__.py | 8 +-
superset/extensions/ssh.py | 17 +--
superset/initialization/__init__.py | 4 +-
superset/utils/class_utils.py | 39 +++++++
superset/utils/machine_auth.py | 17 +--
superset/views/core.py | 2 +-
tests/integration_tests/charts/data/api_tests.py | 18 +--
tests/integration_tests/core_tests.py | 6 +-
tests/integration_tests/test_app.py | 7 +-
18 files changed, 233 insertions(+), 62 deletions(-)
diff --git a/.gitignore b/.gitignore
index a23cbb9ba5..4e69678246 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,7 @@ superset/bin/supersetc
tmp
rat-results.txt
superset/app/
+superset-websocket/config.json
# Node.js, webpack artifacts, storybook
*.entry.js
diff --git a/docker-compose.yml b/docker-compose.yml
index dc9f9d5589..8a017356cb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,6 +29,14 @@ x-superset-volumes: &superset-volumes
version: "3.7"
services:
+ nginx:
+ image: nginx:latest
+ container_name: superset_nginx
+ restart: unless-stopped
+ ports:
+ - "80:80"
+ volumes:
+ - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
redis:
image: redis:7
container_name: superset_cache
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
new file mode 100644
index 0000000000..eda47ef580
--- /dev/null
+++ b/docker/nginx/nginx.conf
@@ -0,0 +1,127 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+user nginx;
+worker_processes 1;
+
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent [$connection_requests]
"$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 30;
+ keepalive_requests 2;
+
+ ###### Compression Stuff
+
+ # Enable Gzip compressed.
+ gzip on;
+
+ # Compression level (1-9).
+ # 5 is a perfect compromise between size and cpu usage, offering about
+ # 75% reduction for most ascii files (almost identical to level 9).
+ gzip_comp_level 5;
+
+ # Don't compress anything that's already small and unlikely to shrink much
+ # if at all (the default is 20 bytes, which is bad as that usually leads to
+ # larger files after gzipping).
+ gzip_min_length 256;
+
+ # Compress data even for clients that are connecting to us via proxies,
+ # identified by the "Via" header (required for CloudFront).
+ gzip_proxied any;
+
+ # Tell proxies to cache both the gzipped and regular version of a resource
+ # whenever the client's Accept-Encoding capabilities header varies;
+ # Avoids the issue where a non-gzip capable client (which is extremely rare
+ # today) would display gibberish if their proxy gave them the gzipped
version.
+ gzip_vary on;
+
+ # Compress all output labeled with one of the following MIME-types.
+ gzip_types
+ application/atom+xml
+ application/javascript
+ application/json
+ application/rss+xml
+ application/vnd.ms-fontobject
+ application/x-font-ttf
+ application/x-web-app-manifest+json
+ application/xhtml+xml
+ application/xml
+ font/opentype
+ image/svg+xml
+ image/x-icon
+ text/css
+ text/plain
+ text/x-component;
+ # text/html is always compressed by HttpGzipModule
+
+ output_buffers 20 10m;
+
+ client_max_body_size 10m;
+
+ upstream superset_app {
+ server host.docker.internal:8088;
+ keepalive 100;
+ }
+
+ upstream superset_websocket {
+ server host.docker.internal:8080;
+ keepalive 100;
+ }
+
+ server {
+ listen 80 default_server;
+ server_name _;
+
+ location /ws {
+ proxy_pass http://superset_websocket;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ proxy_set_header Host $host;
+ }
+
+ location / {
+ proxy_pass http://superset_app;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_http_version 1.1;
+ port_in_redirect off;
+ proxy_connect_timeout 300;
+ }
+ }
+}
diff --git a/superset/app.py b/superset/app.py
index 48883a9264..a2192b8966 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -17,6 +17,7 @@
import logging
import os
+from typing import Optional
from flask import Flask
@@ -25,12 +26,14 @@ from superset.initialization import SupersetAppInitializer
logger = logging.getLogger(__name__)
-def create_app() -> Flask:
+def create_app(superset_config_module: Optional[str] = None) -> Flask:
app = SupersetApp(__name__)
try:
# Allow user to override our config completely
- config_module = os.environ.get("SUPERSET_CONFIG", "superset.config")
+ config_module = superset_config_module or os.environ.get(
+ "SUPERSET_CONFIG", "superset.config"
+ )
app.config.from_object(config_module)
app_initializer = app.config.get("APP_INITIALIZER",
SupersetAppInitializer)(app)
diff --git a/superset/async_events/api.py b/superset/async_events/api.py
index 8b682c396a..0a6ceb9c5f 100644
--- a/superset/async_events/api.py
+++ b/superset/async_events/api.py
@@ -21,8 +21,8 @@ from flask_appbuilder import expose
from flask_appbuilder.api import safe
from flask_appbuilder.security.decorators import permission_name, protect
+from superset.async_events.async_query_manager import AsyncQueryTokenException
from superset.extensions import async_query_manager, event_logger
-from superset.utils.async_query_manager import AsyncQueryTokenException
from superset.views.base_api import BaseSupersetApi
logger = logging.getLogger(__name__)
diff --git a/superset/utils/async_query_manager.py
b/superset/async_events/async_query_manager.py
similarity index 100%
rename from superset/utils/async_query_manager.py
rename to superset/async_events/async_query_manager.py
diff --git a/tests/integration_tests/test_app.py
b/superset/async_events/async_query_manager_factory.py
similarity index 56%
copy from tests/integration_tests/test_app.py
copy to superset/async_events/async_query_manager_factory.py
index fb7b47b67c..2e05f38603 100644
--- a/tests/integration_tests/test_app.py
+++ b/superset/async_events/async_query_manager_factory.py
@@ -14,23 +14,22 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from typing import TYPE_CHECKING
-from superset.app import create_app
+from flask import Flask
-if TYPE_CHECKING:
- from typing import Any
+from superset.async_events.async_query_manager import AsyncQueryManager
+from superset.utils.class_utils import load_class_from_name
- from flask.testing import FlaskClient
-app = create_app()
+class AsyncQueryManagerFactory:
+ def __init__(self) -> None:
+ self._async_query_manager: AsyncQueryManager = None # type: ignore
+ def init_app(self, app: Flask) -> None:
+ self._async_query_manager = load_class_from_name(
+ app.config["GLOBAL_ASYNC_QUERY_MANAGER_CLASS"]
+ )()
+ self._async_query_manager.init_app(app)
-def login(
- client: "FlaskClient[Any]", username: str = "admin", password: str =
"general"
-):
- resp = client.post(
- "/login/",
- data=dict(username=username, password=password),
- ).get_data(as_text=True)
- assert "User confirmation needed" not in resp
+ def instance(self) -> AsyncQueryManager:
+ return self._async_query_manager
diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py
index 1e26bfab31..c8ed840c7c 100644
--- a/superset/charts/data/api.py
+++ b/superset/charts/data/api.py
@@ -28,6 +28,7 @@ from flask_babel import gettext as _
from marshmallow import ValidationError
from superset import is_feature_enabled, security_manager
+from superset.async_events.async_query_manager import AsyncQueryTokenException
from superset.charts.api import ChartRestApi
from superset.charts.commands.exceptions import (
ChartDataCacheLoadError,
@@ -46,7 +47,6 @@ from superset.daos.exceptions import DatasourceNotFound
from superset.exceptions import QueryObjectValidationError
from superset.extensions import event_logger
from superset.models.sql_lab import Query
-from superset.utils.async_query_manager import AsyncQueryTokenException
from superset.utils.core import create_zip, get_user_id, json_int_dttm_ser
from superset.views.base import CsvResponse, generate_download_headers,
XlsxResponse
from superset.views.base_api import statsd_metrics
diff --git a/superset/config.py b/superset/config.py
index 1145a7693f..74f5df0e6e 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1503,6 +1503,9 @@ SQLA_TABLE_MUTATOR = lambda table: table
# Global async query config options.
# Requires GLOBAL_ASYNC_QUERIES feature flag to be enabled.
+GLOBAL_ASYNC_QUERY_MANAGER_CLASS = (
+ "superset.async_events.async_query_manager.AsyncQueryManager"
+)
GLOBAL_ASYNC_QUERIES_REDIS_CONFIG = {
"port": 6379,
"host": "127.0.0.1",
diff --git a/superset/extensions/__init__.py b/superset/extensions/__init__.py
index 42daf8205b..c68332738b 100644
--- a/superset/extensions/__init__.py
+++ b/superset/extensions/__init__.py
@@ -27,9 +27,10 @@ from flask_talisman import Talisman
from flask_wtf.csrf import CSRFProtect
from werkzeug.local import LocalProxy
+from superset.async_events.async_query_manager import AsyncQueryManager
+from superset.async_events.async_query_manager_factory import
AsyncQueryManagerFactory
from superset.extensions.ssh import SSHManagerFactory
from superset.extensions.stats_logger import BaseStatsLoggerManager
-from superset.utils.async_query_manager import AsyncQueryManager
from superset.utils.cache_manager import CacheManager
from superset.utils.encrypt import EncryptedFieldFactory
from superset.utils.feature_flag_manager import FeatureFlagManager
@@ -114,7 +115,10 @@ class ProfilingExtension: # pylint:
disable=too-few-public-methods
APP_DIR = os.path.join(os.path.dirname(__file__), os.path.pardir)
appbuilder = AppBuilder(update_perms=False)
-async_query_manager = AsyncQueryManager()
+async_query_manager_factory = AsyncQueryManagerFactory()
+async_query_manager: AsyncQueryManager = LocalProxy(
+ async_query_manager_factory.instance
+)
cache_manager = CacheManager()
celery_app = celery.Celery()
csrf = CSRFProtect()
diff --git a/superset/extensions/ssh.py b/superset/extensions/ssh.py
index 0a88bf70cb..09840cc38b 100644
--- a/superset/extensions/ssh.py
+++ b/superset/extensions/ssh.py
@@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-import importlib
import logging
from io import StringIO
from typing import TYPE_CHECKING
@@ -25,6 +24,7 @@ from flask import Flask
from paramiko import RSAKey
from superset.databases.utils import make_url_safe
+from superset.utils.class_utils import load_class_from_name
if TYPE_CHECKING:
from superset.databases.ssh_tunnel.models import SSHTunnel
@@ -78,18 +78,9 @@ class SSHManagerFactory:
self._ssh_manager = None
def init_app(self, app: Flask) -> None:
- ssh_manager_fqclass = app.config["SSH_TUNNEL_MANAGER_CLASS"]
- ssh_manager_classname = ssh_manager_fqclass[
- ssh_manager_fqclass.rfind(".") + 1 :
- ]
- ssh_manager_module_name = ssh_manager_fqclass[
- 0 : ssh_manager_fqclass.rfind(".")
- ]
- ssh_manager_class = getattr(
- importlib.import_module(ssh_manager_module_name),
ssh_manager_classname
- )
-
- self._ssh_manager = ssh_manager_class(app)
+ self._ssh_manager = load_class_from_name(
+ app.config["SSH_TUNNEL_MANAGER_CLASS"]
+ )(app)
@property
def instance(self) -> SSHManager:
diff --git a/superset/initialization/__init__.py
b/superset/initialization/__init__.py
index 61205f38df..1cab4b1bf5 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -35,7 +35,7 @@ from superset.extensions import (
_event_logger,
APP_DIR,
appbuilder,
- async_query_manager,
+ async_query_manager_factory,
cache_manager,
celery_app,
csrf,
@@ -665,7 +665,7 @@ class SupersetAppInitializer: # pylint:
disable=too-many-public-methods
def configure_async_queries(self) -> None:
if feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES"):
- async_query_manager.init_app(self.superset_app)
+ async_query_manager_factory.init_app(self.superset_app)
def register_blueprints(self) -> None:
for bp in self.config["BLUEPRINTS"]:
diff --git a/superset/utils/class_utils.py b/superset/utils/class_utils.py
new file mode 100644
index 0000000000..f79467108a
--- /dev/null
+++ b/superset/utils/class_utils.py
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from importlib import import_module
+from typing import Any
+
+
+def load_class_from_name(fq_class_name: str) -> Any:
+ """
+ Given a string representing a fully qualified class name, attempts to load
+ the class and return it.
+
+ :param fq_class_name: The fully qualified name of the class to load
+ :return: The class object
+ :raises Exception: if the class cannot be loaded
+ """
+ if not fq_class_name:
+ raise ValueError(f"Invalid class name {fq_class_name}")
+
+ parts = fq_class_name.split(".")
+ module_name = ".".join(parts[:-1])
+ class_name = parts[-1]
+
+ module = import_module(module_name)
+ return getattr(module, class_name)
diff --git a/superset/utils/machine_auth.py b/superset/utils/machine_auth.py
index d8b8e709d1..1340ddbdc6 100644
--- a/superset/utils/machine_auth.py
+++ b/superset/utils/machine_auth.py
@@ -17,7 +17,6 @@
from __future__ import annotations
-import importlib
import logging
from typing import Callable, TYPE_CHECKING
@@ -26,6 +25,7 @@ from flask_login import login_user
from selenium.webdriver.remote.webdriver import WebDriver
from werkzeug.http import parse_cookie
+from superset.utils.class_utils import load_class_from_name
from superset.utils.urls import headless_url
logger = logging.getLogger(__name__)
@@ -100,18 +100,9 @@ class MachineAuthProviderFactory:
self._auth_provider = None
def init_app(self, app: Flask) -> None:
- auth_provider_fqclass = app.config["MACHINE_AUTH_PROVIDER_CLASS"]
- auth_provider_classname = auth_provider_fqclass[
- auth_provider_fqclass.rfind(".") + 1 :
- ]
- auth_provider_module_name = auth_provider_fqclass[
- 0 : auth_provider_fqclass.rfind(".")
- ]
- auth_provider_class = getattr(
- importlib.import_module(auth_provider_module_name),
auth_provider_classname
- )
-
- self._auth_provider =
auth_provider_class(app.config["WEBDRIVER_AUTH_FUNC"])
+ self._auth_provider = load_class_from_name(
+ app.config["MACHINE_AUTH_PROVIDER_CLASS"]
+ )(app.config["WEBDRIVER_AUTH_FUNC"])
@property
def instance(self) -> MachineAuthProvider:
diff --git a/superset/views/core.py b/superset/views/core.py
index a12fbf6b19..268c6fe333 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -43,6 +43,7 @@ from superset import (
is_feature_enabled,
security_manager,
)
+from superset.async_events.async_query_manager import AsyncQueryTokenException
from superset.charts.commands.exceptions import ChartNotFoundError
from superset.charts.commands.warm_up_cache import ChartWarmUpCacheCommand
from superset.common.chart_data import ChartDataResultFormat,
ChartDataResultType
@@ -75,7 +76,6 @@ from superset.sqllab.utils import bootstrap_sqllab_data
from superset.superset_typing import FlaskResponse
from superset.tasks.async_queries import load_explore_json_into_cache
from superset.utils import core as utils
-from superset.utils.async_query_manager import AsyncQueryTokenException
from superset.utils.cache import etag_cache
from superset.utils.core import (
base_json_conv,
diff --git a/tests/integration_tests/charts/data/api_tests.py
b/tests/integration_tests/charts/data/api_tests.py
index ab91cce55e..32a4be160c 100644
--- a/tests/integration_tests/charts/data/api_tests.py
+++ b/tests/integration_tests/charts/data/api_tests.py
@@ -45,7 +45,7 @@ from superset.models.slice import Slice
from superset.charts.data.commands.get_data_command import ChartDataCommand
from superset.connectors.sqla.models import TableColumn, SqlaTable
from superset.errors import SupersetErrorType
-from superset.extensions import async_query_manager, db
+from superset.extensions import async_query_manager_factory, db
from superset.models.annotations import AnnotationLayer
from superset.models.slice import Slice
from superset.superset_typing import AdhocColumn
@@ -626,7 +626,7 @@ class TestPostChartDataApi(BaseTestChartDataApi):
def test_chart_data_async(self):
self.logout()
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
self.login("admin")
rv = self.post_assert_metric(CHART_DATA_URI,
self.query_context_payload, "data")
self.assertEqual(rv.status_code, 202)
@@ -644,7 +644,7 @@ class TestPostChartDataApi(BaseTestChartDataApi):
when results are already cached.
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
class QueryContext:
result_format = ChartDataResultFormat.JSON
@@ -674,7 +674,7 @@ class TestPostChartDataApi(BaseTestChartDataApi):
Chart data API: Test chart data query non-JSON format (async)
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
self.query_context_payload["result_type"] = "results"
rv = self.post_assert_metric(CHART_DATA_URI,
self.query_context_payload, "data")
self.assertEqual(rv.status_code, 200)
@@ -686,7 +686,7 @@ class TestPostChartDataApi(BaseTestChartDataApi):
Chart data API: Test chart data query (async)
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
test_client.set_cookie(
"localhost", app.config["GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME"],
"foo"
)
@@ -1066,7 +1066,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
Chart data cache API: Test chart data async cache request
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
cache_loader.load.return_value = self.query_context_payload
orig_run = ChartDataCommand.run
@@ -1093,7 +1093,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
Chart data cache API: Test chart data async cache request with run
failure
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
cache_loader.load.return_value = self.query_context_payload
rv = self.get_assert_metric(
f"{CHART_DATA_URI}/test-cache-key", "data_from_cache"
@@ -1111,7 +1111,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
Chart data cache API: Test chart data async cache request (no login)
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
self.logout()
cache_loader.load.return_value = self.query_context_payload
orig_run = ChartDataCommand.run
@@ -1134,7 +1134,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
Chart data cache API: Test chart data async cache request with invalid
cache key
"""
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
rv = self.get_assert_metric(
f"{CHART_DATA_URI}/test-cache-key", "data_from_cache"
)
diff --git a/tests/integration_tests/core_tests.py
b/tests/integration_tests/core_tests.py
index a10b3974b3..5f379e2c47 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -42,7 +42,7 @@ from superset.connectors.sqla.models import SqlaTable
from superset.db_engine_specs.base import BaseEngineSpec
from superset.db_engine_specs.mssql import MssqlEngineSpec
from superset.exceptions import SupersetException
-from superset.extensions import async_query_manager, cache_manager
+from superset.extensions import async_query_manager_factory, cache_manager
from superset.models import core as models
from superset.models.cache import CacheKey
from superset.models.dashboard import Dashboard
@@ -705,7 +705,7 @@ class TestCore(SupersetTestCase):
"row_limit": 100,
}
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
self.login(username="admin")
rv = self.client.post(
"/superset/explore_json/",
@@ -737,7 +737,7 @@ class TestCore(SupersetTestCase):
"row_limit": 100,
}
app._got_first_request = False
- async_query_manager.init_app(app)
+ async_query_manager_factory.init_app(app)
self.login(username="admin")
rv = self.client.post(
"/superset/explore_json/?results=true",
diff --git a/tests/integration_tests/test_app.py
b/tests/integration_tests/test_app.py
index fb7b47b67c..cd5692939c 100644
--- a/tests/integration_tests/test_app.py
+++ b/tests/integration_tests/test_app.py
@@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+from os import environ
from typing import TYPE_CHECKING
from superset.app import create_app
@@ -23,7 +24,11 @@ if TYPE_CHECKING:
from flask.testing import FlaskClient
-app = create_app()
+
+superset_config_module = environ.get(
+ "SUPERSET_CONFIG", "tests.integration_tests.superset_test_config"
+)
+app = create_app(superset_config_module=superset_config_module)
def login(