From 332dc61fd474cd0808199fb3654328ed58f76375 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Fri, 29 Nov 2019 14:20:03 +0100 Subject: [PATCH] add notification and valid returns --- src/risotto/dispatcher.py | 100 ++++++++++++++++++++++---- src/risotto/http.py | 4 ++ src/risotto/message/message.py | 84 +++++++++------------- src/risotto/services/config/config.py | 10 ++- 4 files changed, 134 insertions(+), 64 deletions(-) diff --git a/src/risotto/dispatcher.py b/src/risotto/dispatcher.py index 013120e..15106c6 100644 --- a/src/risotto/dispatcher.py +++ b/src/risotto/dispatcher.py @@ -2,7 +2,7 @@ from tiramisu import Config from inspect import signature from traceback import print_exc from copy import copy -from typing import Dict +from typing import Dict, Callable from .utils import undefined, _ from .error import RegistrationError, CallError, NotAllowedError @@ -18,7 +18,10 @@ def register(uri: str, """ version, uri = uri.split('.', 1) def decorator(function): - dispatcher.set_function(version, uri, function) + dispatcher.set_function(version, + uri, + notification, + function) return decorator @@ -100,7 +103,11 @@ class RegisterDispatcher: msg = _(f'extra arguments: {extra_function_args}') raise RegistrationError(_(f'error with {module_name}.{function_name} arguments: {msg}')) - def set_function(self, version, uri, function): + def set_function(self, + version: str, + uri: str, + notification: str, + function: Callable): """ register a function to an URI URI is a message """ @@ -145,11 +152,18 @@ class RegisterDispatcher: # valid function's arguments self._valid_rpc_params(version, uri, function, module_name) # register this function - self.uris[version][uri] = {'module': module_name, - 'function': function, - 'risotto_context': inject_risotto_context} + dico = {'module': module_name, + 'function': function, + 'risotto_context': inject_risotto_context} + if notification is undefined: + raise RegistrationError(_('notification is mandatory when registered {uri} with {module_name}.{function_name} even if you set None')) + if notification: + dico['notification'] = notification + self.uris[version][uri] = dico else: # if event + if notification and notification is not undefined: + raise RegistrationError(_(f'notification not supported yet')) # valid function's arguments self._valid_event_params(version, uri, function, module_name) # register this function @@ -164,7 +178,7 @@ class RegisterDispatcher: """ register and instanciate a new module """ try: - self.modules[module_name] = module.Risotto() + self.injected_self[module_name] = module.Risotto() except AttributeError as err: raise RegistrationError(_(f'unable to register the module {module_name}, this module must have Risotto class')) @@ -182,11 +196,13 @@ class Dispatcher(RegisterDispatcher): so launch a function when a message is called """ def __init__(self): - self.modules = {} + # reference to instanciate module (to inject self in method): {"module_name": instance_of_module} + self.injected_self = {} + # list of uris with informations: {"v1": {"module_name.xxxxx": yyyyyy}} self.uris = {} + # all function for a module, to avoid conflict name {"v1": {"module_name": ["function_name"]}} self.function_names = {} self.messages, self.option = get_messages() - config = Config(self.option) def new_context(self, context: Context, @@ -246,6 +262,51 @@ class Dispatcher(RegisterDispatcher): # return the config return config + def valid_call_returns(self, + function: Callable, + returns: Dict, + version: str, + uri:str, + context: Context, + kwargs: Dict): + if not isinstance(returns, dict): + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} has to return a dict') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) + response = self.messages[uri]['response'] + if response is None: + raise Exception('hu?') + else: + config = Config(response, display_name=lambda self, dyn_name: self.impl_getname()) + config.property.read_write() + try: + for key, value in returns.items(): + config.option(key).value.set(value) + except AttributeError: + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} return the unknown parameter "{key}"') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) + except ValueError: + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} return the parameter "{key}" with an unvalid value "{value}"') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) + config.property.read_only() + try: + config.value.dict() + except Exception as err: + module_name = function.__module__.split('.')[-2] + function_name = function.__name__ + err = _(f'function {module_name}.{function_name} return an invalid response {err}') + log.error_msg(version, uri, context, kwargs, 'call', err) + raise CallError(str(err)) + + async def call(self, version, uri, risotto_context, public_only=False, **kwargs): """ execute the function associate with specified uri arguments are validate before @@ -270,7 +331,7 @@ class Dispatcher(RegisterDispatcher): kw = config.option(uri).value.dict() if obj['risotto_context']: kw['risotto_context'] = new_context - returns = await obj['function'](self.modules[obj['module']], **kw) + returns = await obj['function'](self.injected_self[obj['module']], **kw) except CallError as err: raise err except Exception as err: @@ -278,9 +339,22 @@ class Dispatcher(RegisterDispatcher): print_exc() log.error_msg(version, uri, new_context, kwargs, 'call', err) raise CallError(str(err)) - # FIXME notification - # FIXME valider le retour! + # valid returns + self.valid_call_returns(obj['function'], + returns, + version, + uri, + new_context, + kwargs) + # log the success log.info_msg(version, uri, new_context, kwargs, 'call', _(f'returns {returns}')) + # notification + if obj.get('notification'): + notif_version, notif_message = obj['notification'].split('.', 1) + await self.publish(notif_version, + notif_message, + new_context, + **returns) return returns async def publish(self, version, uri, risotto_context, public_only=False, **kwargs): @@ -320,7 +394,7 @@ class Dispatcher(RegisterDispatcher): if function_obj['risotto_context']: kw['risotto_context'] = new_context # send event - await function(self.modules[function_obj['module']], **kw) + await function(self.injected_self[function_obj['module']], **kw) except Exception as err: if DEBUG: print_exc() diff --git a/src/risotto/http.py b/src/risotto/http.py index 03cf593..3eb1740 100644 --- a/src/risotto/http.py +++ b/src/risotto/http.py @@ -7,6 +7,8 @@ from .context import Context from .error import CallError, NotAllowedError from .message import get_messages from .logger import log +from .config import DEBUG +from traceback import print_exc async def handle(request): @@ -25,6 +27,8 @@ async def handle(request): except CallError as err: raise HTTPBadRequest(reason=str(err)) except Exception as err: + if DEBUG: + print_exc() raise HTTPInternalServerError(reason=str(err)) return Response(text=dumps({'response': text})) diff --git a/src/risotto/message/message.py b/src/risotto/message/message.py index 208224e..6d52f03 100644 --- a/src/risotto/message/message.py +++ b/src/risotto/message/message.py @@ -448,53 +448,39 @@ def _parse_responses(message_def, file_path): """build option with returns """ - responses = OrderedDict() - if message_def.response: - keys = {'': {'description': '', - 'columns': {}}} - provides = {} - to_list = True - param_type = message_def.response.type + if message_def.response.parameters is None: + raise Exception('not implemented yet') + #name = 'response' + #keys['']['columns'][name] = {'description': message_def.response.description, + # 'type': message_def.response.type} + #responses = {} + #responses['keys'] = keys + #return responses - if param_type.startswith('[]'): - to_list = False - param_type = param_type[2:] - if param_type in ['Dict', 'File']: - pass + options = [] + names = [] + for name, obj in message_def.response.parameters.items(): + if name in names: + raise Exception('multi response with name {} in {}'.format(name, file_path)) + names.append(name) - if message_def.response.parameters is not None: - for name, obj in message_def.response.parameters.items(): - if name in responses: - raise Exception('multi response with name {} in {}'.format(name, file_path)) - - descr = obj.description.strip().rstrip() - keys['']['columns'][name] = {'description': obj.description, - 'type': obj.type} - ref = obj.ref - if ref: - provides[name] = ref - else: - keys['']['columns'][name] = {'description': descr, - 'type': obj.type} - ref = obj.ref - if ref: - provides[name] = ref - responses['keys'] = keys - responses['to_list'] = to_list - responses['to_dict'] = False - responses['provides'] = provides + option = {'String': StrOption, + 'Number': IntOption, + 'Boolean': BoolOption, + # FIXME + 'File': StrOption}.get(obj.type) + if not option: + raise Exception(f'unknown param type {obj.type}') + kwargs = {'name': name, + 'doc': obj.description.strip().rstrip()} + if hasattr(obj, 'default'): + kwargs['default'] = obj.default else: - name = 'response' - keys['']['columns'][name] = {'description': message_def.response.description, - 'type': message_def.response.type} - ref = message_def.response.ref - if ref: - provides[name] = ref - responses['keys'] = keys - responses['to_list'] = to_list - responses['to_dict'] = True - responses['provides'] = provides - return responses + kwargs['properties'] = ('mandatory',) + options.append(option(**kwargs)) + return OptionDescription(message_def.uri, + message_def.response.description, + options) def _getoptions_from_yml(message_def, @@ -564,7 +550,6 @@ def get_messages(load_shortarg=False, only_public=False): optiondescriptions = OrderedDict() optiondescriptions_name = [] optiondescriptions_info = {} - responses = OrderedDict() needs = OrderedDict() messages = list(list_messages()) messages.sort() @@ -586,9 +571,12 @@ def get_messages(load_shortarg=False, only_public=False): continue optiondescriptions_info[message_def.uri] = {'pattern': message_def.pattern, 'public': message_def.public} + if message_def.pattern == 'rpc': + optiondescriptions_info[message_def.uri]['response'] = _parse_responses(message_def, + message_name) + elif message_def.response: + raise Exception(f'response not allowed for {message_def.uri}') version = message_name.split('.')[0] - if message_def.uri in responses: - raise Exception('uri {} allready loader'.format(message_def.uri)) _getoptions_from_yml(message_def, version, optiondescriptions, @@ -596,8 +584,6 @@ def get_messages(load_shortarg=False, only_public=False): needs, select_option, load_shortarg) - responses[message_def.uri] = _parse_responses(message_def, - message_name) root = _get_root_option(select_option, optiondescriptions) try: diff --git a/src/risotto/services/config/config.py b/src/risotto/services/config/config.py index f53d9b7..3e7051c 100644 --- a/src/risotto/services/config/config.py +++ b/src/risotto/services/config/config.py @@ -11,7 +11,13 @@ class Risotto(Controller): async def get_configuration(self, risotto_context, id): #stop = await self.call('v1.config.session.server.stop', risotto_context, sessionid=str(id)) #await self.publish('v1.config.configuration.server.updated', risotto_context, server_id=1, deploy=True) - return {'start': id} + return {'id': id, + 'sessionid': 'sess', + 'username': risotto_context.username, + 'timestamp': 0, + 'namespace': 'creole', + 'mode': 'basic', + 'debug': False} @register('v1.config.session.server.stop', None) async def get_configuration2(self, sessionid, save): @@ -21,6 +27,6 @@ class Risotto(Controller): async def get_configuration3(self, server_id, deploy): return {'get': server_id} - @register('v1.config.configuration.server.deploy', None) + @register('v1.config.configuration.server.deploy') async def get_configuration4(self, server_id): return {'deploy': server_id}