diff --git a/redash/__init__.py b/redash/__init__.py index 790549de63..6450111874 100644 --- a/redash/__init__.py +++ b/redash/__init__.py @@ -1,28 +1,22 @@ -import sys import logging -import urlparse +import os +import sys import urllib +import urlparse import redis -from flask import Flask, current_app -from flask_sslify import SSLify -from werkzeug.contrib.fixers import ProxyFix -from werkzeug.routing import BaseConverter -from statsd import StatsClient from flask_mail import Mail from flask_limiter import Limiter from flask_limiter.util import get_ipaddr from flask_migrate import Migrate +from statsd import StatsClient +from werkzeug.local import LocalProxy -from redash import settings -from redash.query_runner import import_query_runners -from redash.destinations import import_destinations - +from . import settings __version__ = '7.0.0-beta' -import os if os.environ.get("REMOTE_DEBUG"): import ptvsd ptvsd.enable_attach(address=('0.0.0.0', 5678)) @@ -37,10 +31,8 @@ def setup_logging(): # Make noisy libraries less noisy if settings.LOG_LEVEL != "DEBUG": - logging.getLogger("passlib").setLevel("ERROR") - logging.getLogger("requests.packages.urllib3").setLevel("ERROR") - logging.getLogger("snowflake.connector").setLevel("ERROR") - logging.getLogger('apiclient').setLevel("ERROR") + for name in ["passlib", "requests.packages.urllib3", "snowflake.connector", "apiclient"]: + logging.getLogger(name).setLevel("ERROR") def create_redis_connection(): @@ -68,71 +60,13 @@ def create_redis_connection(): setup_logging() -redis_connection = create_redis_connection() - -mail = Mail() -migrate = Migrate() -mail.init_mail(settings.all_settings()) -statsd_client = StatsClient(host=settings.STATSD_HOST, port=settings.STATSD_PORT, prefix=settings.STATSD_PREFIX) -limiter = Limiter(key_func=get_ipaddr, storage_uri=settings.LIMITER_STORAGE) -import_query_runners(settings.QUERY_RUNNERS) -import_destinations(settings.DESTINATIONS) +redis_connection = LocalProxy(create_redis_connection) -from redash.version_check import reset_new_version_status -reset_new_version_status() +mail = LocalProxy(Mail) +migrate = LocalProxy(Migrate) -class SlugConverter(BaseConverter): - def to_python(self, value): - # This is ay workaround for when we enable multi-org and some files are being called by the index rule: - # for path in settings.STATIC_ASSETS_PATHS: - # full_path = safe_join(path, value) - # if os.path.isfile(full_path): - # raise ValidationError() - - return value - - def to_url(self, value): - return value - - -def create_app(): - from redash import authentication, extensions, handlers - from redash.handlers.webpack import configure_webpack - from redash.handlers import chrome_logger - from redash.models import db, users - from redash.metrics.request import provision_app - from redash.utils import sentry - - sentry.init() - - app = Flask(__name__, - template_folder=settings.STATIC_ASSETS_PATH, - static_folder=settings.STATIC_ASSETS_PATH, - static_path='/static') - - # Make sure we get the right referral address even behind proxies like nginx. - app.wsgi_app = ProxyFix(app.wsgi_app, settings.PROXIES_COUNT) - app.url_map.converters['org_slug'] = SlugConverter - - if settings.ENFORCE_HTTPS: - SSLify(app, skips=['ping']) - - # configure our database - app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI - app.config.update(settings.all_settings()) - - provision_app(app) - db.init_app(app) - migrate.init_app(app, db) - mail.init_app(app) - authentication.init_app(app) - limiter.init_app(app) - handlers.init_app(app) - configure_webpack(app) - extensions.init_extensions(app) - chrome_logger.init_app(app) - users.init_app(app) +statsd_client = StatsClient(host=settings.STATSD_HOST, port=settings.STATSD_PORT, prefix=settings.STATSD_PREFIX) - return app +limiter = Limiter(key_func=get_ipaddr, storage_uri=settings.LIMITER_STORAGE) diff --git a/redash/app.py b/redash/app.py new file mode 100644 index 0000000000..f0d8819ce9 --- /dev/null +++ b/redash/app.py @@ -0,0 +1,62 @@ +from flask import Flask +from flask_sslify import SSLify +from werkzeug.contrib.fixers import ProxyFix + +from . import mail, settings +from .utils import routes + + +class Redash(Flask): + """A custom Flask app for Redash""" + def __init__(self, *args, **kwargs): + kwargs.update({ + 'template_folder': settings.STATIC_ASSETS_PATH, + 'static_folder': settings.STATIC_ASSETS_PATH, + 'static_path': '/static', + }) + super(Redash, self).__init__(__name__, *args, **kwargs) + # Make sure we get the right referral address even behind proxies like nginx. + self.wsgi_app = ProxyFix(self.wsgi_app, settings.PROXIES_COUNT) + # Handle the special case of the org slug + self.url_map.converters['org_slug'] = routes.SlugConverter + # Configure Redash using our settings + self.config.from_object('redash.settings') + + +def create_app(): + from . import authentication, extensions, handlers, limiter, migrate + from .destinations import import_destinations + from .handlers import chrome_logger + from .handlers.webpack import configure_webpack + from .metrics.request import provision_app + from .models import db, users + from .query_runner import import_query_runners + from .utils import sentry + from .version_check import reset_new_version_status + + sentry.init() + app = Redash() + + # Check and update the cached version for use by the client + app.before_first_request(reset_new_version_status) + + # Load query runners and destinations + import_query_runners(settings.QUERY_RUNNERS) + import_destinations(settings.DESTINATIONS) + + if settings.ENFORCE_HTTPS: + SSLify(app, skips=['ping']) + + provision_app(app) + db.init_app(app) + migrate.init_app(app, db) + mail.init_app(app) + authentication.init_app(app) + limiter.init_app(app) + handlers.init_app(app) + configure_webpack(app) + extensions.init_extensions(app) + chrome_logger.init_app(app) + users.init_app(app) + + return app diff --git a/redash/cli/__init__.py b/redash/cli/__init__.py index 316e426d5d..78a10c35cb 100644 --- a/redash/cli/__init__.py +++ b/redash/cli/__init__.py @@ -4,7 +4,8 @@ from flask.cli import FlaskGroup, run_command from flask import current_app -from redash import create_app, settings, __version__ +from redash import settings, __version__ +from redash.app import create_app from redash.cli import users, groups, database, data_sources, organization from redash.monitor import get_status @@ -15,9 +16,11 @@ def create(group): @app.shell_context_processor def shell_context(): - from redash import models - return dict(models=models) - + from redash import models, settings + return { + 'models': models, + 'settings': settings, + } return app @@ -48,7 +51,7 @@ def status(): @manager.command() def check_settings(): """Show the settings as Redash sees them (useful for debugging).""" - for name, item in settings.all_settings().iteritems(): + for name, item in current_app.config.iteritems(): print("{} = {}".format(name, item)) diff --git a/redash/metrics/celery.py b/redash/metrics/celery.py index 6760d96b68..6d972f000e 100644 --- a/redash/metrics/celery.py +++ b/redash/metrics/celery.py @@ -1,17 +1,13 @@ -from __future__ import absolute_import - import logging import socket import time -from celery.signals import task_postrun, task_prerun from redash import settings, statsd_client from redash.utils import json_dumps tasks_start_time = {} -@task_prerun.connect def task_prerun_handler(signal, sender, task_id, task, args, kwargs, **kw): try: tasks_start_time[task_id] = time.time() @@ -29,7 +25,6 @@ def metric_name(name, tags): return "{},{}".format(name, tags_string) -@task_postrun.connect def task_postrun_handler(signal, sender, task_id, task, args, kwargs, retval, state, **kw): try: run_time = 1000 * (time.time() - tasks_start_time.pop(task_id)) diff --git a/redash/monitor.py b/redash/monitor.py index c04235e0a8..c81ddf5a57 100644 --- a/redash/monitor.py +++ b/redash/monitor.py @@ -1,7 +1,5 @@ -import ast import itertools import json -import base64 from sqlalchemy import union_all from redash import redis_connection, __version__, settings from redash.models import db, DataSource, Query, QueryResult, Dashboard, Widget diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py index efb2a1cd25..373eb6514e 100644 --- a/redash/settings/__init__.py +++ b/redash/settings/__init__.py @@ -2,19 +2,7 @@ from funcy import distinct, remove from .helpers import fix_assets_path, array_from_string, parse_boolean, int_or_none, set_from_string -from .organization import DATE_FORMAT - - -def all_settings(): - from types import ModuleType - - settings = {} - for name, item in globals().iteritems(): - if not callable(item) and not name.startswith("__") and not isinstance(item, ModuleType): - settings[name] = item - - return settings - +from .organization import DATE_FORMAT # noqa REDIS_URL = os.environ.get('REDASH_REDIS_URL', os.environ.get('REDIS_URL', "redis://localhost:6379/0")) PROXIES_COUNT = int(os.environ.get('REDASH_PROXIES_COUNT', "1")) diff --git a/redash/tasks/queries.py b/redash/tasks/queries.py index a83ab3242e..1f0236d2d2 100644 --- a/redash/tasks/queries.py +++ b/redash/tasks/queries.py @@ -11,7 +11,7 @@ from redash import models, redis_connection, settings, statsd_client from redash.query_runner import InterruptException from redash.tasks.alerts import check_alerts_for_query -from redash.utils import gen_query_hash, json_dumps, json_loads, utcnow, mustache_render +from redash.utils import gen_query_hash, json_dumps, utcnow, mustache_render from redash.worker import celery logger = get_task_logger(__name__) diff --git a/redash/utils/routes.py b/redash/utils/routes.py new file mode 100644 index 0000000000..00a7af0388 --- /dev/null +++ b/redash/utils/routes.py @@ -0,0 +1,15 @@ +from werkzeug.routing import BaseConverter + + +class SlugConverter(BaseConverter): + def to_python(self, value): + # This is ay workaround for when we enable multi-org and some files are being called by the index rule: + # for path in settings.STATIC_ASSETS_PATHS: + # full_path = safe_join(path, value) + # if os.path.isfile(full_path): + # raise ValidationError() + + return value + + def to_url(self, value): + return value diff --git a/redash/worker.py b/redash/worker.py index 23c3d7b6fe..d99f151fd0 100644 --- a/redash/worker.py +++ b/redash/worker.py @@ -7,9 +7,12 @@ from celery import Celery from celery.schedules import crontab -from celery.signals import worker_process_init -from redash import create_app, settings -from redash.metrics import celery as celery_metrics +from celery.signals import task_postrun, task_prerun, worker_process_init + +from redash import settings +from redash.app import create_app +from redash.metrics.celery import task_postrun_handler, task_prerun_handler + celery = Celery('redash', broker=settings.CELERY_BROKER, @@ -66,6 +69,10 @@ def __call__(self, *args, **kwargs): celery.Task = ContextTask +# Connect the task signal handles for Redash metrics +task_prerun.connect(task_prerun_handler) +task_postrun.connect(task_postrun_handler) + # Create Flask app after forking a new worker, to make sure no resources are shared between processes. @worker_process_init.connect diff --git a/redash/wsgi.py b/redash/wsgi.py index 8c94f55037..a8c20bbe6f 100644 --- a/redash/wsgi.py +++ b/redash/wsgi.py @@ -1,3 +1,3 @@ -from redash import create_app +from redash.app import create_app app = create_app() diff --git a/tests/__init__.py b/tests/__init__.py index 62c720b223..3db2db4d83 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -13,7 +13,7 @@ os.environ['REDASH_GOOGLE_CLIENT_SECRET'] = "dummy" os.environ['REDASH_MULTI_ORG'] = "true" -from redash import create_app +from redash.app import create_app from redash import redis_connection from redash.models import db from redash.utils import json_dumps, json_loads