This is an automated email from the ASF dual-hosted git repository.
amitmiran 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 2792ddc refactor: move SupersetAppInitializer to specific
initialization package (#15278)
2792ddc is described below
commit 2792ddc9b5f8e6d1e7d2cca8d68209bde01a1440
Author: ofekisr <[email protected]>
AuthorDate: Mon Jun 21 13:22:23 2021 +0300
refactor: move SupersetAppInitializer to specific initialization package
(#15278)
---
superset/app.py | 692 +-----------------------
superset/{app.py => initialization/__init__.py} | 31 +-
2 files changed, 8 insertions(+), 715 deletions(-)
diff --git a/superset/app.py b/superset/app.py
index 6a6deb0..2d89ffa 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -17,36 +17,10 @@
import logging
import os
-from typing import Any, Callable, Dict
-import wtforms_json
-from flask import Flask, redirect
-from flask_appbuilder import expose, IndexView
-from flask_babel import gettext as __, lazy_gettext as _
-from flask_compress import Compress
+from flask import Flask
-from superset.connectors.connector_registry import ConnectorRegistry
-from superset.extensions import (
- _event_logger,
- APP_DIR,
- appbuilder,
- async_query_manager,
- cache_manager,
- celery_app,
- csrf,
- db,
- encrypted_field_factory,
- feature_flag_manager,
- machine_auth_provider_factory,
- manifest_processor,
- migrate,
- results_backend_manager,
- talisman,
-)
-from superset.security import SupersetSecurityManager
-from superset.typing import FlaskResponse
-from superset.utils.core import pessimistic_connection_handling
-from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value
+from superset.initialization import SupersetAppInitializer
logger = logging.getLogger(__name__)
@@ -68,665 +42,3 @@ def create_app() -> Flask:
except Exception as ex:
logger.exception("Failed to create app")
raise ex
-
-
-class SupersetIndexView(IndexView):
- @expose("/")
- def index(self) -> FlaskResponse:
- return redirect("/superset/welcome/")
-
-
-# pylint: disable=R0904
-class SupersetAppInitializer:
- def __init__(self, app: Flask) -> None:
- super().__init__()
-
- self.flask_app = app
- self.config = app.config
- self.manifest: Dict[Any, Any] = {}
-
- def pre_init(self) -> None:
- """
- Called before all other init tasks are complete
- """
- wtforms_json.init()
-
- if not os.path.exists(self.config["DATA_DIR"]):
- os.makedirs(self.config["DATA_DIR"])
-
- def post_init(self) -> None:
- """
- Called after any other init tasks
- """
-
- def configure_celery(self) -> None:
- celery_app.config_from_object(self.config["CELERY_CONFIG"])
- celery_app.set_default()
- flask_app = self.flask_app
-
- # Here, we want to ensure that every call into Celery task has an app
context
- # setup properly
- task_base = celery_app.Task
-
- class AppContextTask(task_base): # type: ignore
- # pylint: disable=too-few-public-methods
- abstract = True
-
- # Grab each call into the task and set up an app context
- def __call__(self, *args: Any, **kwargs: Any) -> Any:
- with flask_app.app_context(): # type: ignore
- return task_base.__call__(self, *args, **kwargs)
-
- celery_app.Task = AppContextTask
-
- def init_views(self) -> None:
- #
- # We're doing local imports, as several of them import
- # models which in turn try to import
- # the global Flask app
- #
- # pylint: disable=too-many-locals
- # pylint: disable=too-many-statements
- # pylint: disable=too-many-branches
- from superset.annotation_layers.api import AnnotationLayerRestApi
- from superset.annotation_layers.annotations.api import
AnnotationRestApi
- from superset.async_events.api import AsyncEventsRestApi
- from superset.cachekeys.api import CacheRestApi
- from superset.charts.api import ChartRestApi
- from superset.connectors.druid.views import (
- Druid,
- DruidClusterModelView,
- DruidColumnInlineView,
- DruidDatasourceModelView,
- DruidMetricInlineView,
- )
- from superset.connectors.sqla.views import (
- RowLevelSecurityFiltersModelView,
- SqlMetricInlineView,
- TableColumnInlineView,
- TableModelView,
- )
- from superset.css_templates.api import CssTemplateRestApi
- from superset.dashboards.api import DashboardRestApi
- from superset.databases.api import DatabaseRestApi
- from superset.datasets.api import DatasetRestApi
- from superset.datasets.columns.api import DatasetColumnsRestApi
- from superset.datasets.metrics.api import DatasetMetricRestApi
- from superset.queries.api import QueryRestApi
- from superset.security.api import SecurityRestApi
- from superset.queries.saved_queries.api import SavedQueryRestApi
- from superset.reports.api import ReportScheduleRestApi
- from superset.reports.logs.api import ReportExecutionLogRestApi
- from superset.views.access_requests import AccessRequestsModelView
- from superset.views.alerts import (
- AlertLogModelView,
- AlertModelView,
- AlertObservationModelView,
- AlertView,
- ReportView,
- )
- from superset.views.annotations import (
- AnnotationLayerModelView,
- AnnotationModelView,
- )
- from superset.views.api import Api
- from superset.views.chart.views import SliceAsync, SliceModelView
- from superset.views.core import Superset
- from superset.views.css_templates import (
- CssTemplateAsyncModelView,
- CssTemplateModelView,
- )
- from superset.views.dashboard.views import (
- Dashboard,
- DashboardModelView,
- DashboardModelViewAsync,
- )
- from superset.views.database.views import (
- CsvToDatabaseView,
- DatabaseView,
- ExcelToDatabaseView,
- )
- from superset.views.datasource import Datasource
- from superset.views.dynamic_plugins import DynamicPluginsView
- from superset.views.key_value import KV
- from superset.views.log.api import LogRestApi
- from superset.views.log.views import LogModelView
- from superset.views.redirects import R
- from superset.views.schedules import (
- DashboardEmailScheduleView,
- SliceEmailScheduleView,
- )
- from superset.views.sql_lab import (
- SavedQueryView,
- SavedQueryViewApi,
- SqlLab,
- TableSchemaView,
- TabStateView,
- )
- from superset.views.tags import TagView
-
- #
- # Setup API views
- #
- appbuilder.add_api(AnnotationRestApi)
- appbuilder.add_api(AnnotationLayerRestApi)
- appbuilder.add_api(AsyncEventsRestApi)
- appbuilder.add_api(CacheRestApi)
- appbuilder.add_api(ChartRestApi)
- appbuilder.add_api(CssTemplateRestApi)
- appbuilder.add_api(DashboardRestApi)
- appbuilder.add_api(DatabaseRestApi)
- appbuilder.add_api(DatasetRestApi)
- appbuilder.add_api(DatasetColumnsRestApi)
- appbuilder.add_api(DatasetMetricRestApi)
- appbuilder.add_api(QueryRestApi)
- appbuilder.add_api(SavedQueryRestApi)
- appbuilder.add_api(ReportScheduleRestApi)
- appbuilder.add_api(ReportExecutionLogRestApi)
- #
- # Setup regular views
- #
- appbuilder.add_link(
- "Home",
- label=__("Home"),
- href="/superset/welcome/",
- cond=lambda: bool(appbuilder.app.config["LOGO_TARGET_PATH"]),
- )
- appbuilder.add_view(
- AnnotationLayerModelView,
- "Annotation Layers",
- label=__("Annotation Layers"),
- icon="fa-comment",
- category="Manage",
- category_label=__("Manage"),
- category_icon="",
- )
- appbuilder.add_view(
- DatabaseView,
- "Databases",
- label=__("Databases"),
- icon="fa-database",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-database",
- )
- appbuilder.add_link(
- "Datasets",
- label=__("Datasets"),
- href="/tablemodelview/list/",
- icon="fa-table",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-table",
- )
- appbuilder.add_separator("Data")
- appbuilder.add_view(
- SliceModelView,
- "Charts",
- label=__("Charts"),
- icon="fa-bar-chart",
- category="",
- category_icon="",
- )
- appbuilder.add_view(
- DashboardModelView,
- "Dashboards",
- label=__("Dashboards"),
- icon="fa-dashboard",
- category="",
- category_icon="",
- )
- appbuilder.add_view(
- DynamicPluginsView,
- "Plugins",
- label=__("Plugins"),
- category="Manage",
- category_label=__("Manage"),
- icon="fa-puzzle-piece",
- menu_cond=lambda: feature_flag_manager.is_feature_enabled(
- "DYNAMIC_PLUGINS"
- ),
- )
- appbuilder.add_view(
- CssTemplateModelView,
- "CSS Templates",
- label=__("CSS Templates"),
- icon="fa-css3",
- category="Manage",
- category_label=__("Manage"),
- category_icon="",
- )
- appbuilder.add_view(
- RowLevelSecurityFiltersModelView,
- "Row Level Security",
- label=__("Row level security"),
- category="Security",
- category_label=__("Security"),
- icon="fa-lock",
- menu_cond=lambda: feature_flag_manager.is_feature_enabled(
- "ROW_LEVEL_SECURITY"
- ),
- )
-
- #
- # Setup views with no menu
- #
- appbuilder.add_view_no_menu(Api)
- appbuilder.add_view_no_menu(CssTemplateAsyncModelView)
- appbuilder.add_view_no_menu(CsvToDatabaseView)
- appbuilder.add_view_no_menu(ExcelToDatabaseView)
- appbuilder.add_view_no_menu(Dashboard)
- appbuilder.add_view_no_menu(DashboardModelViewAsync)
- appbuilder.add_view_no_menu(Datasource)
- appbuilder.add_view_no_menu(KV)
- appbuilder.add_view_no_menu(R)
- appbuilder.add_view_no_menu(SavedQueryView)
- appbuilder.add_view_no_menu(SavedQueryViewApi)
- appbuilder.add_view_no_menu(SliceAsync)
- appbuilder.add_view_no_menu(SqlLab)
- appbuilder.add_view_no_menu(SqlMetricInlineView)
- appbuilder.add_view_no_menu(AnnotationModelView)
- appbuilder.add_view_no_menu(Superset)
- appbuilder.add_view_no_menu(TableColumnInlineView)
- appbuilder.add_view_no_menu(TableModelView)
- appbuilder.add_view_no_menu(TableSchemaView)
- appbuilder.add_view_no_menu(TabStateView)
- appbuilder.add_view_no_menu(TagView)
-
- #
- # Add links
- #
- appbuilder.add_link(
- "Import Dashboards",
- label=__("Import Dashboards"),
- href="/superset/import_dashboards/",
- icon="fa-cloud-upload",
- category="Manage",
- category_label=__("Manage"),
- category_icon="fa-wrench",
- cond=lambda: not feature_flag_manager.is_feature_enabled(
- "VERSIONED_EXPORT"
- ),
- )
- appbuilder.add_link(
- "SQL Editor",
- label=_("SQL Editor"),
- href="/superset/sqllab/",
- category_icon="fa-flask",
- icon="fa-flask",
- category="SQL Lab",
- category_label=__("SQL Lab"),
- )
- appbuilder.add_link(
- __("Saved Queries"),
- href="/savedqueryview/list/",
- icon="fa-save",
- category="SQL Lab",
- )
- appbuilder.add_link(
- "Query Search",
- label=_("Query History"),
- href="/superset/sqllab/history/",
- icon="fa-search",
- category_icon="fa-flask",
- category="SQL Lab",
- category_label=__("SQL Lab"),
- )
- appbuilder.add_link(
- "Upload a CSV",
- label=__("Upload a CSV"),
- href="/csvtodatabaseview/form",
- icon="fa-upload",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-wrench",
- cond=lambda: bool(
- self.config["CSV_EXTENSIONS"].intersection(
- self.config["ALLOWED_EXTENSIONS"]
- )
- ),
- )
-
- try:
- import xlrd # pylint: disable=unused-import
-
- appbuilder.add_link(
- "Upload Excel",
- label=__("Upload Excel"),
- href="/exceltodatabaseview/form",
- icon="fa-upload",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-wrench",
- cond=lambda: bool(
- self.config["EXCEL_EXTENSIONS"].intersection(
- self.config["ALLOWED_EXTENSIONS"]
- )
- ),
- )
- except ImportError:
- pass
-
- appbuilder.add_api(LogRestApi)
- appbuilder.add_view(
- LogModelView,
- "Action Log",
- label=__("Action Log"),
- category="Security",
- category_label=__("Security"),
- icon="fa-list-ol",
- menu_cond=lambda: (
- self.config["FAB_ADD_SECURITY_VIEWS"]
- and self.config["SUPERSET_LOG_VIEW"]
- ),
- )
- appbuilder.add_api(SecurityRestApi)
- #
- # Conditionally setup email views
- #
- if self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"]:
- logging.warning(
- "ENABLE_SCHEDULED_EMAIL_REPORTS "
- "is deprecated and will be removed in version 2.0.0"
- )
-
- appbuilder.add_separator(
- "Manage", cond=lambda:
self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"]
- )
- appbuilder.add_view(
- DashboardEmailScheduleView,
- "Dashboard Email Schedules",
- label=__("Dashboard Emails"),
- category="Manage",
- category_label=__("Manage"),
- icon="fa-search",
- menu_cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"],
- )
- appbuilder.add_view(
- SliceEmailScheduleView,
- "Chart Emails",
- label=__("Chart Email Schedules"),
- category="Manage",
- category_label=__("Manage"),
- icon="fa-search",
- menu_cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"],
- )
-
- if self.config["ENABLE_ALERTS"]:
- logging.warning(
- "ENABLE_ALERTS is deprecated and will be removed in version
2.0.0"
- )
-
- appbuilder.add_view(
- AlertModelView,
- "Alerts",
- label=__("Alerts"),
- category="Manage",
- category_label=__("Manage"),
- icon="fa-exclamation-triangle",
- menu_cond=lambda: bool(self.config["ENABLE_ALERTS"]),
- )
- appbuilder.add_view_no_menu(AlertLogModelView)
- appbuilder.add_view_no_menu(AlertObservationModelView)
-
- appbuilder.add_view(
- AlertView,
- "Alerts & Report",
- label=__("Alerts & Reports"),
- category="Manage",
- category_label=__("Manage"),
- icon="fa-exclamation-triangle",
- menu_cond=lambda:
feature_flag_manager.is_feature_enabled("ALERT_REPORTS"),
- )
- appbuilder.add_view_no_menu(ReportView)
-
- appbuilder.add_view(
- AccessRequestsModelView,
- "Access requests",
- label=__("Access requests"),
- category="Security",
- category_label=__("Security"),
- icon="fa-table",
- menu_cond=lambda: bool(self.config["ENABLE_ACCESS_REQUEST"]),
- )
-
- #
- # Druid Views
- #
- appbuilder.add_separator(
- "Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"])
- )
- appbuilder.add_view(
- DruidDatasourceModelView,
- "Druid Datasources",
- label=__("Druid Datasources"),
- category="Data",
- category_label=__("Data"),
- icon="fa-cube",
- menu_cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]),
- )
- appbuilder.add_view(
- DruidClusterModelView,
- name="Druid Clusters",
- label=__("Druid Clusters"),
- icon="fa-cubes",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-database",
- menu_cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]),
- )
- appbuilder.add_view_no_menu(DruidMetricInlineView)
- appbuilder.add_view_no_menu(DruidColumnInlineView)
- appbuilder.add_view_no_menu(Druid)
-
- appbuilder.add_link(
- "Scan New Datasources",
- label=__("Scan New Datasources"),
- href="/druid/scan_new_datasources/",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-database",
- icon="fa-refresh",
- cond=lambda: bool(
- self.config["DRUID_IS_ACTIVE"]
- and self.config["DRUID_METADATA_LINKS_ENABLED"]
- ),
- )
- appbuilder.add_link(
- "Refresh Druid Metadata",
- label=__("Refresh Druid Metadata"),
- href="/druid/refresh_datasources/",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-database",
- icon="fa-cog",
- cond=lambda: bool(
- self.config["DRUID_IS_ACTIVE"]
- and self.config["DRUID_METADATA_LINKS_ENABLED"]
- ),
- )
- appbuilder.add_separator(
- "Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"])
- )
-
- def init_app_in_ctx(self) -> None:
- """
- Runs init logic in the context of the app
- """
- self.configure_fab()
- self.configure_url_map_converters()
- self.configure_data_sources()
- self.configure_auth_provider()
- self.configure_async_queries()
-
- # Hook that provides administrators a handle on the Flask APP
- # after initialization
- flask_app_mutator = self.config["FLASK_APP_MUTATOR"]
- if flask_app_mutator:
- flask_app_mutator(self.flask_app)
-
- self.init_views()
-
- def init_app(self) -> None:
- """
- Main entry point which will delegate to other methods in
- order to fully init the app
- """
- self.pre_init()
- # Configuration of logging must be done first to apply the formatter
properly
- self.configure_logging()
- # Configuration of feature_flags must be done first to allow init
features
- # conditionally
- self.configure_feature_flags()
- self.configure_db_encrypt()
- self.setup_db()
- self.configure_celery()
- self.setup_event_logger()
- self.setup_bundle_manifest()
- self.register_blueprints()
- self.configure_wtf()
- self.configure_middlewares()
- self.configure_cache()
-
- with self.flask_app.app_context(): # type: ignore
- self.init_app_in_ctx()
-
- self.post_init()
-
- def configure_auth_provider(self) -> None:
- machine_auth_provider_factory.init_app(self.flask_app)
-
- def setup_event_logger(self) -> None:
- _event_logger["event_logger"] = get_event_logger_from_cfg_value(
- self.flask_app.config.get("EVENT_LOGGER", DBEventLogger())
- )
-
- def configure_data_sources(self) -> None:
- # Registering sources
- module_datasource_map = self.config["DEFAULT_MODULE_DS_MAP"]
- module_datasource_map.update(self.config["ADDITIONAL_MODULE_DS_MAP"])
- ConnectorRegistry.register_sources(module_datasource_map)
-
- def configure_cache(self) -> None:
- cache_manager.init_app(self.flask_app)
- results_backend_manager.init_app(self.flask_app)
-
- def configure_feature_flags(self) -> None:
- feature_flag_manager.init_app(self.flask_app)
-
- def configure_fab(self) -> None:
- if self.config["SILENCE_FAB"]:
- logging.getLogger("flask_appbuilder").setLevel(logging.ERROR)
-
- custom_sm = self.config["CUSTOM_SECURITY_MANAGER"] or
SupersetSecurityManager
- if not issubclass(custom_sm, SupersetSecurityManager):
- raise Exception(
- """Your CUSTOM_SECURITY_MANAGER must now extend
SupersetSecurityManager,
- not FAB's security manager.
- See [4565] in UPDATING.md"""
- )
-
- appbuilder.indexview = SupersetIndexView
- appbuilder.base_template = "superset/base.html"
- appbuilder.security_manager_class = custom_sm
- appbuilder.init_app(self.flask_app, db.session)
-
- def configure_url_map_converters(self) -> None:
- #
- # Doing local imports here as model importing causes a reference to
- # app.config to be invoked and we need the current_app to have been
setup
- #
- from superset.utils.url_map_converters import (
- ObjectTypeConverter,
- RegexConverter,
- )
-
- self.flask_app.url_map.converters["regex"] = RegexConverter
- self.flask_app.url_map.converters["object_type"] = ObjectTypeConverter
-
- def configure_middlewares(self) -> None:
- if self.config["ENABLE_CORS"]:
- from flask_cors import CORS
-
- CORS(self.flask_app, **self.config["CORS_OPTIONS"])
-
- if self.config["ENABLE_PROXY_FIX"]:
- from werkzeug.middleware.proxy_fix import ProxyFix
-
- self.flask_app.wsgi_app = ProxyFix( # type: ignore
- self.flask_app.wsgi_app, **self.config["PROXY_FIX_CONFIG"]
- )
-
- if self.config["ENABLE_CHUNK_ENCODING"]:
-
- class ChunkedEncodingFix: # pylint: disable=too-few-public-methods
- def __init__(self, app: Flask) -> None:
- self.app = app
-
- def __call__(
- self, environ: Dict[str, Any], start_response:
Callable[..., Any]
- ) -> Any:
- # Setting wsgi.input_terminated tells werkzeug.wsgi to
ignore
- # content-length and read the stream till the end.
- if environ.get("HTTP_TRANSFER_ENCODING", "").lower() ==
"chunked":
- environ["wsgi.input_terminated"] = True
- return self.app(environ, start_response)
-
- self.flask_app.wsgi_app = ChunkedEncodingFix( # type: ignore
- self.flask_app.wsgi_app # type: ignore
- )
-
- if self.config["UPLOAD_FOLDER"]:
- try:
- os.makedirs(self.config["UPLOAD_FOLDER"])
- except OSError:
- pass
-
- for middleware in self.config["ADDITIONAL_MIDDLEWARE"]:
- self.flask_app.wsgi_app = middleware( # type: ignore
- self.flask_app.wsgi_app
- )
-
- # Flask-Compress
- Compress(self.flask_app)
-
- if self.config["TALISMAN_ENABLED"]:
- talisman.init_app(self.flask_app, **self.config["TALISMAN_CONFIG"])
-
- def configure_logging(self) -> None:
- self.config["LOGGING_CONFIGURATOR"].configure_logging(
- self.config, self.flask_app.debug
- )
-
- def configure_db_encrypt(self) -> None:
- encrypted_field_factory.init_app(self.flask_app)
-
- def setup_db(self) -> None:
- db.init_app(self.flask_app)
-
- with self.flask_app.app_context(): # type: ignore
- pessimistic_connection_handling(db.engine)
-
- migrate.init_app(self.flask_app, db=db, directory=APP_DIR +
"/migrations")
-
- def configure_wtf(self) -> None:
- if self.config["WTF_CSRF_ENABLED"]:
- csrf.init_app(self.flask_app)
- csrf_exempt_list = self.config["WTF_CSRF_EXEMPT_LIST"]
- for ex in csrf_exempt_list:
- csrf.exempt(ex)
-
- def configure_async_queries(self) -> None:
- if feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES"):
- async_query_manager.init_app(self.flask_app)
-
- def register_blueprints(self) -> None:
- for bp in self.config["BLUEPRINTS"]:
- try:
- logger.info("Registering blueprint: %s", bp.name)
- self.flask_app.register_blueprint(bp)
- except Exception: # pylint: disable=broad-except
- logger.exception("blueprint registration failed")
-
- def setup_bundle_manifest(self) -> None:
- manifest_processor.init_app(self.flask_app)
diff --git a/superset/app.py b/superset/initialization/__init__.py
similarity index 97%
copy from superset/app.py
copy to superset/initialization/__init__.py
index 6a6deb0..0171b17 100644
--- a/superset/app.py
+++ b/superset/initialization/__init__.py
@@ -51,31 +51,6 @@ from superset.utils.log import DBEventLogger,
get_event_logger_from_cfg_value
logger = logging.getLogger(__name__)
-def create_app() -> Flask:
- app = Flask(__name__)
-
- try:
- # Allow user to override our config completely
- config_module = os.environ.get("SUPERSET_CONFIG", "superset.config")
- app.config.from_object(config_module)
-
- app_initializer = app.config.get("APP_INITIALIZER",
SupersetAppInitializer)(app)
- app_initializer.init_app()
-
- return app
-
- # Make sure that bootstrap errors ALWAYS get logged
- except Exception as ex:
- logger.exception("Failed to create app")
- raise ex
-
-
-class SupersetIndexView(IndexView):
- @expose("/")
- def index(self) -> FlaskResponse:
- return redirect("/superset/welcome/")
-
-
# pylint: disable=R0904
class SupersetAppInitializer:
def __init__(self, app: Flask) -> None:
@@ -730,3 +705,9 @@ class SupersetAppInitializer:
def setup_bundle_manifest(self) -> None:
manifest_processor.init_app(self.flask_app)
+
+
+class SupersetIndexView(IndexView):
+ @expose("/")
+ def index(self) -> FlaskResponse:
+ return redirect("/superset/welcome/")