Source code for flask_redis
"""
===========
flask_redis
===========
Simple as dead support of Redis database for Flask apps.
"""
import inspect
import sys
try:
import urllib.parse as urlparse
except ImportError: # pragma: no cover
import urlparse
try:
from flask import _app_ctx_stack
except ImportError: # pragma: no cover
_app_ctx_stack = None
import redis
from flask import _request_ctx_stack
from werkzeug.utils import import_string
__all__ = ('Redis', )
__author = 'Igor Davydenko'
__license__ = 'BSD License'
__version__ = '1.0.0'
IS_PY3 = sys.version_info[0] == 3
IS_REDIS3 = redis.VERSION[0] == 3
string_types = (str if IS_PY3 else basestring, ) # noqa
# Default Redis connection class
RedisClass = redis.Redis if IS_REDIS3 else redis.StrictRedis
# Which stack should we use? _app_ctx_stack is new in 0.9
connection_stack = _app_ctx_stack or _request_ctx_stack
[docs]class Redis(object):
"""Simple as dead support of Redis database for Flask apps."""
[docs] def __init__(self, app=None, config_prefix=None):
"""Initialize Redis extension for Flask application.
If ``app`` argument provided then initialize redis connection using
application config values.
If no ``app`` argument provided you should do initialization later with
:meth:`init_app` method.
Generally extension expects configuration to be prefixed with ``REDIS``
config prefix, to customize things pass different ``config_prefix``
here or on calling :meth:`init_app` method. For example, if you have
URL to Redis in ``CACHE_URL`` config key, you should pass
``config_prefix='CACHE'`` to extension.
:param app: :class:`flask.Flask` application instance.
:param config_prefix: Config prefix to use. By default: ``REDIS``
"""
self.app = app
if app is not None:
self.init_app(app, config_prefix)
@property
def connection(self):
"""Return Redis connection for current app."""
return self.get_app().extensions['redis'][self.config_prefix]
[docs] def get_app(self):
"""Get current app from Flast stack to use.
This will allow to ensure which Redis connection to be used when
accessing Redis connection public methods via plugin.
"""
# First see to connection stack
ctx = connection_stack.top
if ctx is not None:
return ctx.app
# Next return app from instance cache
if self.app is not None:
return self.app
# Something went wrong, in most cases app just not instantiated yet
# and we cannot locate it
raise RuntimeError(
'Flask application not registered on Redis instance '
'and no applcation bound to current context')
[docs] def init_app(self, app, config_prefix=None):
"""
Actual method to read redis settings from app configuration, initialize
Redis connection and copy all public connection methods to current
instance.
:param app: :class:`flask.Flask` application instance.
:param config_prefix: Config prefix to use. By default: ``REDIS``
"""
# Put redis to application extensions
if 'redis' not in app.extensions:
app.extensions['redis'] = {}
# Which config prefix to use, custom or default one?
self.config_prefix = config_prefix = config_prefix or 'REDIS'
# No way to do registration two times
if config_prefix in app.extensions['redis']:
raise ValueError('Already registered config prefix {0!r}.'.
format(config_prefix))
# Start reading configuration, define converters to use and key func
# to prepend config prefix to key value
converters = {'port': int}
convert = lambda arg, value: (converters[arg](value)
if arg in converters
else value)
key = lambda param: '{0}_{1}'.format(config_prefix, param)
# Which redis connection class to use?
klass = app.config.get(key('CLASS'), RedisClass)
# Import connection class if it stil path notation
if isinstance(klass, string_types):
klass = import_string(klass)
# Should we use URL configuration
url = app.config.get(key('URL'))
# If should, parse URL and store values to application config to later
# reuse if necessary
if url:
urlparse.uses_netloc.append('redis')
url = urlparse.urlparse(url)
# URL could contains host, port, user, password and db values
app.config[key('HOST')] = url.hostname
app.config[key('PORT')] = url.port or 6379
app.config[key('USER')] = url.username
app.config[key('PASSWORD')] = url.password
db = url.path.replace('/', '')
app.config[key('DB')] = db if db.isdigit() else None
# Host is not a mandatory key if you want to use connection pool. But
# when present and starts with file:// or / use it as unix socket path
host = app.config.get(key('HOST'))
if host and (host.startswith('file://') or host.startswith('/')):
app.config.pop(key('HOST'))
app.config[key('UNIX_SOCKET_PATH')] = host
args = self._build_connection_args(klass)
kwargs = dict([(arg, convert(arg, app.config[key(arg.upper())]))
for arg in args
if key(arg.upper()) in app.config])
# Initialize connection and store it to extensions
connection = klass(**kwargs)
app.extensions['redis'][config_prefix] = connection
# Include public methods to current instance
self._include_public_methods(connection)
def _build_connection_args(self, klass):
"""Read connection args spec, exclude self from list of possible
:param klass: Redis connection class.
"""
bases = [base for base in klass.__bases__ if base is not object]
all_args = []
for cls in [klass] + bases:
try:
args = inspect.getfullargspec(cls.__init__).args
except AttributeError:
args = inspect.getargspec(cls.__init__).args
for arg in args:
if arg in all_args:
continue
all_args.append(arg)
all_args.remove('self')
return all_args
def _include_public_methods(self, connection):
"""Include public methods from Redis connection to current instance.
:param connection: Redis connection instance.
"""
for attr in dir(connection):
value = getattr(connection, attr)
if attr.startswith('_') or not callable(value):
continue
self.__dict__[attr] = self._wrap_public_method(attr)
def _wrap_public_method(self, attr):
"""
Ensure that plugin will call current connection method when accessing
as ``plugin.<public_method>(*args, **kwargs)``.
"""
def wrapper(*args, **kwargs):
return getattr(self.connection, attr)(*args, **kwargs)
return wrapper