123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- import logging
- import shlex
- import warnings
- from flask import request
- from jinja2 import Markup
- from flask_admin.base import BaseView, expose
- from flask_admin.babel import gettext
- from flask_admin._compat import VER
- # Set up logger
- log = logging.getLogger("flask-admin.redis")
- class CommandError(Exception):
- """
- RedisCli error exception.
- """
- pass
- class TextWrapper(str):
- """
- Small text wrapper for result formatter to distinguish between
- different string types.
- """
- pass
- class RedisCli(BaseView):
- """
- Simple redis console.
- To use it, simply pass `Redis` connection object to the constructor.
- """
- shlex_check = True
- """
- shlex from stdlib does not work with unicode on 2.7.2 and lower.
- If you want to suppress warning, set this attribute to False.
- """
- remapped_commands = {
- 'del': 'delete'
- }
- """
- List of redis remapped commands.
- """
- excluded_commands = set(('pubsub', 'set_response_callback', 'from_url'))
- """
- List of excluded commands.
- """
- def __init__(self, redis,
- name=None, category=None, endpoint=None, url=None):
- """
- Constructor.
- :param redis:
- Redis connection
- :param name:
- View name. If not provided, will use the model class name
- :param category:
- View category
- :param endpoint:
- Base endpoint. If not provided, will use the model name + 'view'.
- For example if model name was 'User', endpoint will be
- 'userview'
- :param url:
- Base URL. If not provided, will use endpoint as a URL.
- """
- super(RedisCli, self).__init__(name, category, endpoint, url)
- self.redis = redis
- self.commands = {}
- self._inspect_commands()
- self._contribute_commands()
- if self.shlex_check and VER < (2, 7, 3):
- warnings.warn('Warning: rediscli uses shlex library and it does '
- 'not work with unicode until Python 2.7.3. To '
- 'remove this warning, upgrade to Python 2.7.3 or '
- 'suppress it by setting shlex_check attribute '
- 'to False.')
- def _inspect_commands(self):
- """
- Inspect connection object and extract command names.
- """
- for name in dir(self.redis):
- if not name.startswith('_'):
- attr = getattr(self.redis, name)
- if callable(attr) and name not in self.excluded_commands:
- doc = (getattr(attr, '__doc__', '') or '').strip()
- self.commands[name] = (attr, doc)
- for new, old in self.remapped_commands.items():
- self.commands[new] = self.commands[old]
- def _contribute_commands(self):
- """
- Contribute custom commands.
- """
- self.commands['help'] = (self._cmd_help, 'Help!')
- def _execute_command(self, name, args):
- """
- Execute single command.
- :param name:
- Command name
- :param args:
- Command arguments
- """
- # Do some remapping
- new_cmd = self.remapped_commands.get(name)
- if new_cmd:
- name = new_cmd
- # Execute command
- if name not in self.commands:
- return self._error(gettext('Cli: Invalid command.'))
- handler, _ = self.commands[name]
- return self._result(handler(*args))
- def _parse_cmd(self, cmd):
- """
- Parse command by using shlex module.
- :param cmd:
- Command to parse
- """
- if VER < (2, 7, 3):
- # shlex can't work with unicode until 2.7.3
- return tuple(x.decode('utf-8') for x in shlex.split(cmd.encode('utf-8')))
- return tuple(shlex.split(cmd))
- def _error(self, msg):
- """
- Format error message as HTTP response.
- :param msg:
- Message to format
- """
- return Markup('<div class="error">%s</div>' % msg)
- def _result(self, result):
- """
- Format result message as HTTP response.
- :param msg:
- Result to format.
- """
- return self.render('admin/rediscli/response.html',
- type_name=lambda d: type(d).__name__,
- result=result)
- # Commands
- def _cmd_help(self, *args):
- """
- Help command implementation.
- """
- if not args:
- help = 'Usage: help <command>.\nList of supported commands: '
- help += ', '.join(n for n in sorted(self.commands))
- return TextWrapper(help)
- cmd = args[0]
- if cmd not in self.commands:
- raise CommandError('Invalid command.')
- help = self.commands[cmd][1]
- if not help:
- return TextWrapper('Command does not have any help.')
- return TextWrapper(help)
- # Views
- @expose('/')
- def console_view(self):
- """
- Console view.
- """
- return self.render('admin/rediscli/console.html')
- @expose('/run/', methods=('POST',))
- def execute_view(self):
- """
- AJAX API.
- """
- try:
- cmd = request.form.get('cmd').lower()
- if not cmd:
- return self._error('Cli: Empty command.')
- parts = self._parse_cmd(cmd)
- if not parts:
- return self._error('Cli: Failed to parse command.')
- return self._execute_command(parts[0], parts[1:])
- except CommandError as err:
- return self._error('Cli: %s' % err)
- except Exception as ex:
- log.exception(ex)
- return self._error('Cli: %s' % ex)
|