diff options
Diffstat (limited to 'sugar_network/document/commands.py')
-rw-r--r-- | sugar_network/document/commands.py | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/sugar_network/document/commands.py b/sugar_network/document/commands.py new file mode 100644 index 0000000..f6aa681 --- /dev/null +++ b/sugar_network/document/commands.py @@ -0,0 +1,351 @@ +# Copyright (C) 2012 Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import logging + +from active_document import env +from active_document.metadata import PropertyMeta +from active_toolkit import enforce + + +_logger = logging.getLogger('active_document.commands') + + +def command(scope, **kwargs): + + def decorate(func): + func.scope = scope + func.kwargs = kwargs + return func + + return decorate + + +volume_command = \ + lambda ** kwargs: command('volume', **kwargs) +volume_command_pre = \ + lambda ** kwargs: command('volume', wrapper='pre', **kwargs) +volume_command_post = \ + lambda ** kwargs: command('volume', wrapper='post', **kwargs) + +directory_command = \ + lambda ** kwargs: command('directory', **kwargs) +directory_command_pre = \ + lambda ** kwargs: command('directory', wrapper='pre', **kwargs) +directory_command_post = \ + lambda ** kwargs: command('directory', wrapper='post', **kwargs) + +document_command = \ + lambda ** kwargs: command('document', **kwargs) +document_command_pre = \ + lambda ** kwargs: command('document', wrapper='pre', **kwargs) +document_command_post = \ + lambda ** kwargs: command('document', wrapper='post', **kwargs) + +property_command = \ + lambda ** kwargs: command('property', **kwargs) +property_command_pre = \ + lambda ** kwargs: command('property', wrapper='pre', **kwargs) +property_command_post = \ + lambda ** kwargs: command('property', wrapper='post', **kwargs) + + +def to_int(value): + if isinstance(value, basestring): + if not value: + return 0 + enforce(value.isdigit(), 'Argument should be an integer value') + return int(value) + return value + + +def to_list(value): + if isinstance(value, basestring): + if value: + return value.split(',') + else: + return [] + return value + + +class CommandNotFound(Exception): + pass + + +class Request(dict): + + content = None + content_stream = None + content_length = None + content_type = None + access_level = env.ACCESS_REMOTE + accept_language = None + commands = None + response = None + + def __init__(self, **kwargs): + dict.__init__(self, kwargs) + self._pos = 0 + + def __getitem__(self, key): + enforce(key in self, 'Cannot find %r request argument', key) + return self.get(key) + + def read(self, size=None): + if self.content_stream is None: + return '' + rest = max(0, self.content_length - self._pos) + size = rest if size is None else min(rest, size) + result = self.content_stream.read(size) + if not result: + return '' + self._pos += len(result) + return result + + def clone(self): + request = type(self)() + request.access_level = self.access_level + request.accept_language = self.accept_language + request.commands = self.commands + return request + + def call(self, method, content=None, content_stream=None, + content_length=None, **kwargs): + enforce(self.commands is not None) + + request = self.clone() + request.update(kwargs) + request['method'] = method + request.content = content + request.content_stream = content_stream + request.content_length = content_length + + return self.commands.call(request, Response()) + + def __repr__(self): + args = ['content_length=%r' % self.content_length, + 'access_level=%r' % self.access_level, + 'accept_language=%r' % self.accept_language, + ] + ['%s=%r' % i for i in self.items()] + return '<active_document.Request %s>' % ' '.join(args) + + +class Response(dict): + + content_length = None + content_type = None + #: UNIX seconds of last modification + last_modified = None + + def __init__(self, *args, **props): + if args: + props = args[0] + dict.__init__(self, props) + + def __repr__(self): + args = ['%s=%r' % i for i in self.items()] + return '<active_document.Response %s>' % ' '.join(args) + + +class CommandsProcessor(object): + + def __init__(self, volume=None, parent=None): + self._commands = { + 'volume': _Commands(), + 'directory': _Commands(), + 'document': _Commands(), + 'property': _Commands(), + } + self.volume = volume + self.parent = parent + self._lang = env.default_lang() + + for scope, kwargs in _scan_class(self.__class__, False): + cmd = _Command((self,), **kwargs) + self._commands[scope].add(cmd) + + if volume is not None: + for directory in volume.values(): + for scope, kwargs in _scan_class(directory.document_class): + cmd = _ObjectCommand(directory, **kwargs) + self._commands[scope].add(cmd) + + def super_call(self, request, response): + raise CommandNotFound() + + def call(self, request, response=None): + cmd = self.resolve(request) + enforce(cmd is not None, CommandNotFound, 'Unsupported command') + + enforce(request.access_level & cmd.access_level, env.Forbidden, + 'Operation is permitted on requester\'s level') + + if response is None: + response = Response() + request.commands = self + request.response = response + + if not request.accept_language: + request.accept_language = [self._lang] + + for arg, cast in cmd.arguments.items(): + if arg not in request: + continue + try: + request[arg] = cast(request[arg]) + except Exception, error: + raise RuntimeError('Cannot typecast %r command argument: %s' % + (arg, error)) + + args = cmd.get_args(request) + + for pre in cmd.pre: + pre(*args, request=request) + + kwargs = {} + for arg in cmd.kwarg_names: + if arg == 'request': + kwargs[arg] = request + elif arg == 'response': + kwargs[arg] = response + elif arg not in kwargs: + kwargs[arg] = request.get(arg) + + result = cmd.callback(*args, **kwargs) + + for post in cmd.post: + result = post(*args, result=result, request=request, + response=response) + + if not response.content_type: + if isinstance(result, PropertyMeta): + response.content_type = result.get('mime_type') + if not response.content_type: + response.content_type = cmd.mime_type + + return result + + def resolve(self, request): + key = (request.get('method', 'GET'), request.get('cmd'), None) + + if 'document' not in request: + return self._commands['volume'].get(key) + + document_key = key[:2] + (request['document'],) + + if 'guid' not in request: + commands = self._commands['directory'] + return commands.get(key) or commands.get(document_key) + + if 'prop' not in request: + commands = self._commands['document'] + return commands.get(key) or commands.get(document_key) + + commands = self._commands['property'] + return commands.get(key) or commands.get(document_key) + + +class _Command(object): + + def __init__(self, args, callback, method='GET', document=None, cmd=None, + mime_type=None, permissions=0, access_level=env.ACCESS_LEVELS, + arguments=None, pre=None, post=None): + self.args = args + self.callback = callback + self.mime_type = mime_type + self.permissions = permissions + self.access_level = access_level + self.kwarg_names = _function_arg_names(callback) + self.key = (method, cmd, document) + self.arguments = arguments or {} + self.pre = pre + self.post = post + + def get_args(self, request): + return self.args + + def __repr__(self): + return '%s(method=%s, cmd=%s, document=%s)' % \ + ((self.callback.__name__,) + self.key) + + +class _ObjectCommand(_Command): + + def __init__(self, directory, **kwargs): + _Command.__init__(self, (), document=directory.metadata.name, **kwargs) + self._directory = directory + + def get_args(self, request): + document = self._directory.get(request['guid']) + document.request = request + return (document,) + + +class _Commands(dict): + + def add(self, cmd): + enforce(cmd.key not in self, 'Command %r already exists', cmd) + self[cmd.key] = cmd + + +def _function_arg_names(func): + if hasattr(func, 'im_func'): + func = func.im_func + if not hasattr(func, 'func_code'): + return [] + code = func.func_code + # `1:` is for skipping the first, `self` or `cls`, argument + return code.co_varnames[1:code.co_argcount] + + +def _scan_class(root_cls, is_document_class=True): + processed = set() + commands = {} + + cls = root_cls + while cls is not None: + for name in dir(cls): + if name in processed: + continue + attr = getattr(cls, name) + if not hasattr(attr, 'scope'): + continue + enforce(not is_document_class or + attr.scope in ('document', 'property'), + 'Wrong scale command') + key = (attr.scope, + attr.kwargs.get('method') or 'GET', + attr.kwargs.get('cmd')) + kwargs = commands.setdefault(key, {'pre': [], 'post': []}) + callback = getattr(root_cls, attr.__name__) + if 'wrapper' not in attr.kwargs: + kwargs.update(attr.kwargs) + kwargs['callback'] = callback + else: + for key in ('arguments',): + if key in attr.kwargs and key not in kwargs: + kwargs[key] = attr.kwargs[key] + kwargs[attr.kwargs['wrapper']].append(callback) + processed.add(name) + cls = cls.__base__ + + for (scope, method, cmd), kwargs in commands.items(): + if 'callback' not in kwargs: + kwargs['method'] = method + if cmd: + kwargs['cmd'] = cmd + kwargs['callback'] = lambda self, request, response: \ + self.super_call(request, response) + yield scope, kwargs |