Skip to content

Commit

Permalink
Flask App factory PR #1 (#8418)
Browse files Browse the repository at this point in the history
* First cut at app factory

* Setting things back to master

* Working with new FLASK_APP

* Still need to refactor Celery

* CLI mostly working

* Working on unit tests

* Moving cli stuff around a bit

* Removing get in config

* Defaulting test config

* Adding flask-testing

* flask-testing casing

* resultsbackend property bug

* Fixing up cli

* Quick fix for KV api

* Working on save slice

* Fixed core_tests

* Fixed utils_tests

* Most tests working - still need to dig into remaining app_context issue in tests

* All tests passing locally - need to update code comments

* Fixing dashboard tests again

* Blacking

* Sorting imports

* linting

* removing envvar mangling

* blacking

* Fixing unit tests

* isorting

* licensing

* fixing mysql tests

* fixing cypress?

* fixing .flaskenv

* fixing test app_ctx

* fixing cypress

* moving manifest processor around

* moving results backend manager around

* Cleaning up __init__ a bit more

* Addressing PR comments

* Addressing PR comments

* Blacking

* Fixes for running celery worker

* Tuning isort

* Blacking
  • Loading branch information
craig-rueda authored and dpgaspar committed Nov 20, 2019
1 parent 300c4ec commit e490414
Show file tree
Hide file tree
Showing 38 changed files with 1,002 additions and 580 deletions.
4 changes: 2 additions & 2 deletions .flaskenv
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
FLASK_APP=superset:app
FLASK_ENV=development
FLASK_APP="superset.app:create_app()"
FLASK_ENV="development"
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
black==19.3b0
coverage==4.5.3
flask-cors==3.0.7
flask-testing==0.7.1
ipdb==0.12
isort==4.3.21
mypy==0.670
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,5 @@ def get_git_sha():
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
tests_require=["flask-testing==0.7.1"],
)
253 changes: 31 additions & 222 deletions superset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,229 +14,38 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# pylint: disable=C,R,W
"""Package's main module!"""
import json
import logging
import os
from copy import deepcopy
from typing import Any, Dict
from flask import current_app, Flask
from werkzeug.local import LocalProxy

import wtforms_json
from flask import Flask, redirect
from flask_appbuilder import AppBuilder, IndexView, SQLA
from flask_appbuilder.baseviews import expose
from flask_compress import Compress
from flask_migrate import Migrate
from flask_talisman import Talisman
from flask_wtf.csrf import CSRFProtect

from superset import config
from superset.app import create_app
from superset.connectors.connector_registry import ConnectorRegistry
from superset.extensions import (
appbuilder,
cache_manager,
db,
event_logger,
feature_flag_manager,
manifest_processor,
results_backend_manager,
security_manager,
talisman,
)
from superset.security import SupersetSecurityManager
from superset.utils.core import pessimistic_connection_handling, setup_cache
from superset.utils.log import get_event_logger_from_cfg_value

wtforms_json.init()

APP_DIR = os.path.dirname(__file__)
CONFIG_MODULE = os.environ.get("SUPERSET_CONFIG", "superset.config")

if not os.path.exists(config.DATA_DIR):
os.makedirs(config.DATA_DIR)

app = Flask(__name__)
app.config.from_object(CONFIG_MODULE) # type: ignore
conf = app.config

#################################################################
# Handling manifest file logic at app start
#################################################################
MANIFEST_FILE = APP_DIR + "/static/assets/dist/manifest.json"
manifest: Dict[Any, Any] = {}


def parse_manifest_json():
global manifest
try:
with open(MANIFEST_FILE, "r") as f:
# the manifest inclues non-entry files
# we only need entries in templates
full_manifest = json.load(f)
manifest = full_manifest.get("entrypoints", {})
except Exception:
pass


def get_js_manifest_files(filename):
if app.debug:
parse_manifest_json()
entry_files = manifest.get(filename, {})
return entry_files.get("js", [])


def get_css_manifest_files(filename):
if app.debug:
parse_manifest_json()
entry_files = manifest.get(filename, {})
return entry_files.get("css", [])


def get_unloaded_chunks(files, loaded_chunks):
filtered_files = [f for f in files if f not in loaded_chunks]
for f in filtered_files:
loaded_chunks.add(f)
return filtered_files


parse_manifest_json()


@app.context_processor
def get_manifest():
return dict(
loaded_chunks=set(),
get_unloaded_chunks=get_unloaded_chunks,
js_manifest=get_js_manifest_files,
css_manifest=get_css_manifest_files,
)


#################################################################

for bp in conf["BLUEPRINTS"]:
try:
print("Registering blueprint: '{}'".format(bp.name))
app.register_blueprint(bp)
except Exception as e:
print("blueprint registration failed")
logging.exception(e)

if conf.get("SILENCE_FAB"):
logging.getLogger("flask_appbuilder").setLevel(logging.ERROR)

db = SQLA(app)

if conf.get("WTF_CSRF_ENABLED"):
csrf = CSRFProtect(app)
csrf_exempt_list = conf.get("WTF_CSRF_EXEMPT_LIST", [])
for ex in csrf_exempt_list:
csrf.exempt(ex)

pessimistic_connection_handling(db.engine)

cache = setup_cache(app, conf.get("CACHE_CONFIG"))
tables_cache = setup_cache(app, conf.get("TABLE_NAMES_CACHE_CONFIG"))

migrate = Migrate(app, db, directory=APP_DIR + "/migrations")

app.config["LOGGING_CONFIGURATOR"].configure_logging(app.config, app.debug)

if app.config["ENABLE_CORS"]:
from flask_cors import CORS

CORS(app, **app.config["CORS_OPTIONS"])

if app.config["ENABLE_PROXY_FIX"]:
from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix( # type: ignore
app.wsgi_app, **app.config["PROXY_FIX_CONFIG"]
)

if app.config["ENABLE_CHUNK_ENCODING"]:

class ChunkedEncodingFix(object):
def __init__(self, app):
self.app = app

def __call__(self, environ, start_response):
# 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() == u"chunked":
environ["wsgi.input_terminated"] = True
return self.app(environ, start_response)

app.wsgi_app = ChunkedEncodingFix(app.wsgi_app) # type: ignore

if app.config["UPLOAD_FOLDER"]:
try:
os.makedirs(app.config["UPLOAD_FOLDER"])
except OSError:
pass

for middleware in app.config["ADDITIONAL_MIDDLEWARE"]:
app.wsgi_app = middleware(app.wsgi_app) # type: ignore


class MyIndexView(IndexView):
@expose("/")
def index(self):
return redirect("/superset/welcome")


custom_sm = app.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"""
)

with app.app_context():
appbuilder = AppBuilder(
app,
db.session,
base_template="superset/base.html",
indexview=MyIndexView,
security_manager_class=custom_sm,
update_perms=False, # Run `superset init` to update FAB's perms
)

security_manager = appbuilder.sm

results_backend = app.config["RESULTS_BACKEND"]
results_backend_use_msgpack = app.config["RESULTS_BACKEND_USE_MSGPACK"]

# Merge user defined feature flags with default feature flags
_feature_flags = app.config["DEFAULT_FEATURE_FLAGS"]
_feature_flags.update(app.config["FEATURE_FLAGS"])

# Event Logger
event_logger = get_event_logger_from_cfg_value(app.config["EVENT_LOGGER"])


def get_feature_flags():
GET_FEATURE_FLAGS_FUNC = app.config["GET_FEATURE_FLAGS_FUNC"]
if GET_FEATURE_FLAGS_FUNC:
return GET_FEATURE_FLAGS_FUNC(deepcopy(_feature_flags))
return _feature_flags


def is_feature_enabled(feature):
"""Utility function for checking whether a feature is turned on"""
return get_feature_flags().get(feature)


# Flask-Compress
if conf.get("ENABLE_FLASK_COMPRESS"):
Compress(app)


talisman = Talisman()

if app.config["TALISMAN_ENABLED"]:
talisman.init_app(app, **app.config["TALISMAN_CONFIG"])

# Hook that provides administrators a handle on the Flask APP
# after initialization
flask_app_mutator = app.config["FLASK_APP_MUTATOR"]
if flask_app_mutator:
flask_app_mutator(app)

from superset import views # noqa isort:skip

# Registering sources
module_datasource_map = app.config["DEFAULT_MODULE_DS_MAP"]
module_datasource_map.update(app.config["ADDITIONAL_MODULE_DS_MAP"])
ConnectorRegistry.register_sources(module_datasource_map)
from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value

# All of the fields located here should be considered legacy. The correct way
# to declare "global" dependencies is to define it in extensions.py,
# then initialize it in app.create_app(). These fields will be removed
# in subsequent PRs as things are migrated towards the factory pattern
app: Flask = current_app
cache = LocalProxy(lambda: cache_manager.cache)
conf = LocalProxy(lambda: current_app.config)
get_feature_flags = feature_flag_manager.get_feature_flags
get_css_manifest_files = manifest_processor.get_css_manifest_files
is_feature_enabled = feature_flag_manager.is_feature_enabled
results_backend = LocalProxy(lambda: results_backend_manager.results_backend)
results_backend_use_msgpack = LocalProxy(
lambda: results_backend_manager.should_use_msgpack
)
tables_cache = LocalProxy(lambda: cache_manager.tables_cache)
Loading

0 comments on commit e490414

Please sign in to comment.