From eb6d56f7ab6ffc3c06a199fb77b9924f5443c9de Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 01:08:10 +0200 Subject: [PATCH 01/14] Simplify(?) interface initialization --- moulinette/__init__.py | 35 +++++++++++++++--------------- moulinette/core.py | 49 ------------------------------------------ 2 files changed, 17 insertions(+), 67 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 4aae1be0..d9df19b1 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from moulinette.core import ( - init_interface, MoulinetteError, MoulinetteSignals, Moulinette18n, @@ -73,8 +72,6 @@ def init(logging_config=None, **kwargs): # Easy access to interfaces - - def api( namespaces, host="localhost", port=80, routes={}, use_websocket=True, use_cache=True ): @@ -93,13 +90,16 @@ def api( instead of using the cached one """ + from moulinette.actionsmap import ActionsMap + from moulinette.interfaces.api import Interface, ActionsMapParser try: - moulinette = init_interface( - "api", - kwargs={"routes": routes, "use_websocket": use_websocket}, - actionsmap={"namespaces": namespaces, "use_cache": use_cache}, - ) - moulinette.run(host, port) + actionsmap = ActionsMap(ActionsMapParser, + namespaces=namespaces, + use_cache=use_cache) + interface = Interface(actionsmap=actionsmap, + routes=routes, + use_websocket=use_websocket) + interface.run(host, port) except MoulinetteError as e: import logging @@ -138,16 +138,15 @@ def cli( class at construction """ + from moulinette.actionsmap import ActionsMap + from moulinette.interfaces.cli import Interface, ActionsMapParser try: - moulinette = init_interface( - "cli", - actionsmap={ - "namespaces": namespaces, - "use_cache": use_cache, - "parser_kwargs": parser_kwargs, - }, - ) - moulinette.run(args, output_as=output_as, password=password, timeout=timeout) + actionsmap = ActionsMap(ActionsMapParser, + namespaces=namespaces, + use_cache=use_cache, + parser_kwargs=parser_kwargs) + interface = Interface(actionsmap=actionsmap) + interface.run(args, output_as=output_as, password=password, timeout=timeout) except MoulinetteError as e: import logging diff --git a/moulinette/core.py b/moulinette/core.py index d3a2299c..f7ef4876 100644 --- a/moulinette/core.py +++ b/moulinette/core.py @@ -5,8 +5,6 @@ import time import json import logging -from importlib import import_module - import moulinette from moulinette.globals import init_moulinette_env @@ -375,53 +373,6 @@ class MoulinetteSignals(object): raise NotImplementedError("this signal is not handled") -# Interfaces & Authenticators management ------------------------------- - - -def init_interface(name, kwargs={}, actionsmap={}): - """Return a new interface instance - - Retrieve the given interface module and return a new instance of its - Interface class. It is initialized with arguments 'kwargs' and - connected to 'actionsmap' if it's an ActionsMap object, otherwise - a new ActionsMap instance will be initialized with arguments - 'actionsmap'. - - Keyword arguments: - - name -- The interface name - - kwargs -- A dict of arguments to pass to Interface - - actionsmap -- Either an ActionsMap instance or a dict of - arguments to pass to ActionsMap - - """ - from moulinette.actionsmap import ActionsMap - - try: - mod = import_module("moulinette.interfaces.%s" % name) - except ImportError as e: - logger.exception("unable to load interface '%s' : %s", name, e) - raise MoulinetteError("error_see_log") - else: - try: - # Retrieve interface classes - parser = mod.ActionsMapParser - interface = mod.Interface - except AttributeError: - logger.exception("unable to retrieve classes of interface '%s'", name) - raise MoulinetteError("error_see_log") - - # Instantiate or retrieve ActionsMap - if isinstance(actionsmap, dict): - amap = ActionsMap(actionsmap.pop("parser", parser), **actionsmap) - elif isinstance(actionsmap, ActionsMap): - amap = actionsmap - else: - logger.error("invalid actionsmap value %r", actionsmap) - raise MoulinetteError("error_see_log") - - return interface(amap, **kwargs) - - # Moulinette core classes ---------------------------------------------- From b6258de2dbea7bc973b17698a41f148ad291dec4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 01:13:40 +0200 Subject: [PATCH 02/14] We don't need to be able to disable websocket --- moulinette/__init__.py | 6 ++--- moulinette/interfaces/api.py | 47 ++++++++++++++---------------------- test/conftest.py | 4 +-- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index d9df19b1..7c91a579 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -73,7 +73,7 @@ def init(logging_config=None, **kwargs): # Easy access to interfaces def api( - namespaces, host="localhost", port=80, routes={}, use_websocket=True, use_cache=True + namespaces, host="localhost", port=80, routes={}, use_cache=True ): """Web server (API) interface @@ -85,7 +85,6 @@ def api( - port -- Server port to bind to - routes -- A dict of additional routes to add in the form of {(method, uri): callback} - - use_websocket -- Serve via WSGI to handle asynchronous responses - use_cache -- False if it should parse the actions map file instead of using the cached one @@ -97,8 +96,7 @@ def api( namespaces=namespaces, use_cache=use_cache) interface = Interface(actionsmap=actionsmap, - routes=routes, - use_websocket=use_websocket) + routes=routes) interface.run(host, port) except MoulinetteError as e: import logging diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index ee3757bc..084e5671 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -10,7 +10,7 @@ from gevent import sleep from gevent.queue import Queue from geventwebsocket import WebSocketError -from bottle import run, request, response, Bottle, HTTPResponse +from bottle import request, response, Bottle, HTTPResponse from bottle import abort from moulinette import msignals, m18n, env @@ -219,22 +219,18 @@ class _ActionsMapPlugin(object): Keyword arguments: - actionsmap -- An ActionsMap instance - - use_websocket -- If true, install a WebSocket on /messages in order - to serve messages coming from the 'display' signal """ name = "actionsmap" api = 2 - def __init__(self, actionsmap, use_websocket, log_queues={}): + def __init__(self, actionsmap, log_queues={}): # Connect signals to handlers msignals.set_handler("authenticate", self._do_authenticate) - if use_websocket: - msignals.set_handler("display", self._do_display) + msignals.set_handler("display", self._do_display) self.actionsmap = actionsmap - self.use_websocket = use_websocket self.log_queues = log_queues # TODO: Save and load secrets? self.secrets = {} @@ -290,13 +286,12 @@ class _ActionsMapPlugin(object): ) # Append messages route - if self.use_websocket: - app.route( - "/messages", - name="messages", - callback=self.messages, - skip=["actionsmap"], - ) + app.route( + "/messages", + name="messages", + callback=self.messages, + skip=["actionsmap"], + ) # Append routes from the actions map for (m, p) in self.actionsmap.parser.routes: @@ -737,14 +732,12 @@ class Interface(BaseInterface): - actionsmap -- The ActionsMap instance to connect to - routes -- A dict of additional routes to add in the form of {(method, path): callback} - - use_websocket -- Serve via WSGI to handle asynchronous responses - log_queues -- A LogQueues object or None to retrieve it from registered logging handlers """ - def __init__(self, actionsmap, routes={}, use_websocket=True, log_queues=None): - self.use_websocket = use_websocket + def __init__(self, actionsmap, routes={}, log_queues=None): # Attempt to retrieve log queues from an APIQueueHandler if log_queues is None: @@ -776,7 +769,7 @@ class Interface(BaseInterface): app.install(filter_csrf) app.install(apiheader) app.install(api18n) - app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues)) + app.install(_ActionsMapPlugin(actionsmap, log_queues)) # Append default routes # app.route(['/api', '/api/'], method='GET', @@ -801,23 +794,19 @@ class Interface(BaseInterface): """ logger.debug( - "starting the server instance in %s:%d with websocket=%s", + "starting the server instance in %s:%d", host, port, - self.use_websocket, ) try: - if self.use_websocket: - from gevent.pywsgi import WSGIServer - from geventwebsocket.handler import WebSocketHandler + from gevent.pywsgi import WSGIServer + from geventwebsocket.handler import WebSocketHandler - server = WSGIServer( - (host, port), self._app, handler_class=WebSocketHandler - ) - server.serve_forever() - else: - run(self._app, host=host, port=port) + server = WSGIServer( + (host, port), self._app, handler_class=WebSocketHandler + ) + server.serve_forever() except IOError as e: logger.exception("unable to start the server instance on %s:%d", host, port) if e.args[0] == errno.EADDRINUSE: diff --git a/test/conftest.py b/test/conftest.py index 6df66806..59c7f832 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -125,7 +125,7 @@ def moulinette_webapi(moulinette): CookiePolicy.return_ok_secure = return_true - moulinette_webapi = moulinette.core.init_interface( + moulinette_webapi = moulinette.init_interface( "api", kwargs={"routes": {}, "use_websocket": False}, actionsmap={"namespaces": ["moulitest"], "use_cache": True}, @@ -148,7 +148,7 @@ def moulinette_cli(moulinette, mocker): help="Log and print debug messages", ) mocker.patch("os.isatty", return_value=True) - moulinette_cli = moulinette.core.init_interface( + moulinette_cli = moulinette.init_interface( "cli", actionsmap={ "namespaces": ["moulitest"], From fce96ad48f43f4603752659acaaabf4016e0a14f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 01:37:02 +0200 Subject: [PATCH 03/14] We don't need to be able to not use the cache... --- moulinette/__init__.py | 11 ++--------- moulinette/actionsmap.py | 26 ++++++++++---------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 7c91a579..141ac60f 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -73,7 +73,7 @@ def init(logging_config=None, **kwargs): # Easy access to interfaces def api( - namespaces, host="localhost", port=80, routes={}, use_cache=True + namespaces, host="localhost", port=80, routes={} ): """Web server (API) interface @@ -85,16 +85,13 @@ def api( - port -- Server port to bind to - routes -- A dict of additional routes to add in the form of {(method, uri): callback} - - use_cache -- False if it should parse the actions map file - instead of using the cached one """ from moulinette.actionsmap import ActionsMap from moulinette.interfaces.api import Interface, ActionsMapParser try: actionsmap = ActionsMap(ActionsMapParser, - namespaces=namespaces, - use_cache=use_cache) + namespaces=namespaces) interface = Interface(actionsmap=actionsmap, routes=routes) interface.run(host, port) @@ -113,7 +110,6 @@ def api( def cli( namespaces, args, - use_cache=True, output_as=None, password=None, timeout=None, @@ -127,8 +123,6 @@ def cli( Keyword arguments: - namespaces -- The list of namespaces to use - args -- A list of argument strings - - use_cache -- False if it should parse the actions map file - instead of using the cached one - output_as -- Output result in another format, see moulinette.interfaces.cli.Interface for possible values - password -- The password to use in case of authentication @@ -141,7 +135,6 @@ def cli( try: actionsmap = ActionsMap(ActionsMapParser, namespaces=namespaces, - use_cache=use_cache, parser_kwargs=parser_kwargs) interface = Interface(actionsmap=actionsmap) interface.run(args, output_as=output_as, password=password, timeout=timeout) diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index 11f7e2be..bccfdb9d 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -405,18 +405,15 @@ class ActionsMap(object): - parser_class -- The BaseActionsMapParser derived class to use for parsing the actions map - namespaces -- The list of namespaces to use - - use_cache -- False if it should parse the actions map file - instead of using the cached one - parser_kwargs -- A dict of arguments to pass to the parser class at construction """ - def __init__(self, parser_class, namespaces=[], use_cache=True, parser_kwargs={}): + def __init__(self, parser_class, namespaces=[], parser_kwargs={}): if not issubclass(parser_class, BaseActionsMapParser): raise ValueError("Invalid parser class '%s'" % parser_class.__name__) self.parser_class = parser_class - self.use_cache = use_cache moulinette_env = init_moulinette_env() DATA_DIR = moulinette_env["DATA_DIR"] @@ -439,21 +436,19 @@ class ActionsMap(object): actionsmap_yml_stat.st_mtime, ) - if use_cache and os.path.exists(actionsmap_pkl): + if os.path.exists(actionsmap_pkl): + self.from_cache = True try: # Attempt to load cache with open(actionsmap_pkl) as f: actionsmaps[n] = pickle.load(f) # TODO: Switch to python3 and catch proper exception except (IOError, EOFError): - self.use_cache = False + self.from_cache = False actionsmaps = self.generate_cache(namespaces) - elif use_cache: # cached file doesn't exists - self.use_cache = False + else: # cache file doesn't exists + self.from_cache = False actionsmaps = self.generate_cache(namespaces) - elif n not in actionsmaps: - with open(actionsmap_yml) as f: - actionsmaps[n] = ordered_yaml_load(f) # Load translations m18n.load_namespace(n) @@ -668,11 +663,10 @@ class ActionsMap(object): An interface relevant's parser object """ - # Get extra parameters - if self.use_cache: - validate_extra = False - else: - validate_extra = True + + # If loading from cache, extra were already checked when cache was + # loaded ? Not sure about this ... old code is a bit mysterious... + validate_extra = not self.from_cache # Instantiate parser # From a34fb7c66590580dd0e495129f8ac1a9c3b38368 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 01:44:18 +0200 Subject: [PATCH 04/14] We don't need to be able to auth in cli --- moulinette/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 141ac60f..42cc7a38 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -111,7 +111,6 @@ def cli( namespaces, args, output_as=None, - password=None, timeout=None, parser_kwargs={}, ): @@ -125,7 +124,6 @@ def cli( - args -- A list of argument strings - output_as -- Output result in another format, see moulinette.interfaces.cli.Interface for possible values - - password -- The password to use in case of authentication - parser_kwargs -- A dict of arguments to pass to the parser class at construction @@ -137,7 +135,7 @@ def cli( namespaces=namespaces, parser_kwargs=parser_kwargs) interface = Interface(actionsmap=actionsmap) - interface.run(args, output_as=output_as, password=password, timeout=timeout) + interface.run(args, output_as=output_as, timeout=timeout) except MoulinetteError as e: import logging From d2f68cb536541b2b737a8994ace2130a14b38e5c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 02:03:09 +0200 Subject: [PATCH 05/14] We don't need to auth in CLI --- moulinette/interfaces/cli.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index 804028bc..5c5e4f60 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -431,7 +431,7 @@ class Interface(BaseInterface): self.actionsmap = actionsmap - def run(self, args, output_as=None, password=None, timeout=None): + def run(self, args, output_as=None, timeout=None): """Run the moulinette Process the action corresponding to the given arguments 'args' @@ -443,7 +443,6 @@ class Interface(BaseInterface): - json: return a JSON encoded string - plain: return a script-readable output - none: do not output the result - - password -- The password to use in case of authentication - timeout -- Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock """ @@ -454,11 +453,7 @@ class Interface(BaseInterface): argcomplete.autocomplete(self.actionsmap.parser._parser) # Set handler for authentication - if password: - msignals.set_handler("authenticate", lambda a: a(password=password)) - else: - if os.isatty(1): - msignals.set_handler("authenticate", self._do_authenticate) + msignals.set_handler("authenticate", self._do_authenticate) try: ret = self.actionsmap.process(args, timeout=timeout) @@ -490,7 +485,11 @@ class Interface(BaseInterface): Handle the core.MoulinetteSignals.authenticate signal. """ - # TODO: Allow token authentication? + # Hmpf we have no-use case in yunohost anymore where we need to auth + # because everything is run as root ... + # I guess we could imagine some yunohost-independant use-case where + # moulinette is used to create a CLI for non-root user that needs to + # auth somehow but hmpf -.- help = authenticator.extra.get("help") msg = m18n.n(help) if help else m18n.g("password") return authenticator(password=self._do_prompt(msg, True, False, color="yellow")) From 57d1b2b6dbc6f7c63d4e93b3b80c26187f791f2c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 02:10:11 +0200 Subject: [PATCH 06/14] Bit of tidying up --- moulinette/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 42cc7a38..4d7bf14c 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -72,9 +72,7 @@ def init(logging_config=None, **kwargs): # Easy access to interfaces -def api( - namespaces, host="localhost", port=80, routes={} -): +def api(namespaces, host="localhost", port=80, routes={}): """Web server (API) interface Run a HTTP server with the moulinette for an API usage. @@ -107,13 +105,7 @@ def api( return 0 -def cli( - namespaces, - args, - output_as=None, - timeout=None, - parser_kwargs={}, -): +def cli(namespaces, args, output_as=None, timeout=None, parser_kwargs={}): """Command line interface Execute an action with the moulinette from the CLI and print its From 1849d1aa3b10f26bf567c03685f36ed272dc1f29 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 03:07:25 +0200 Subject: [PATCH 07/14] Simplify(?) ActionsMap initialization by removing some obscure kwargs handling --- moulinette/__init__.py | 12 +++++------- moulinette/actionsmap.py | 36 ++++++++++-------------------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 4d7bf14c..0e9188ec 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -88,7 +88,7 @@ def api(namespaces, host="localhost", port=80, routes={}): from moulinette.actionsmap import ActionsMap from moulinette.interfaces.api import Interface, ActionsMapParser try: - actionsmap = ActionsMap(ActionsMapParser, + actionsmap = ActionsMap(ActionsMapParser(), namespaces=namespaces) interface = Interface(actionsmap=actionsmap, routes=routes) @@ -105,7 +105,7 @@ def api(namespaces, host="localhost", port=80, routes={}): return 0 -def cli(namespaces, args, output_as=None, timeout=None, parser_kwargs={}): +def cli(namespaces, args, top_parser, output_as=None, timeout=None): """Command line interface Execute an action with the moulinette from the CLI and print its @@ -116,16 +116,14 @@ def cli(namespaces, args, output_as=None, timeout=None, parser_kwargs={}): - args -- A list of argument strings - output_as -- Output result in another format, see moulinette.interfaces.cli.Interface for possible values - - parser_kwargs -- A dict of arguments to pass to the parser - class at construction + - top_parser -- The top parser used to build the ActionsMapParser """ from moulinette.actionsmap import ActionsMap from moulinette.interfaces.cli import Interface, ActionsMapParser try: - actionsmap = ActionsMap(ActionsMapParser, - namespaces=namespaces, - parser_kwargs=parser_kwargs) + actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser), + namespaces=namespaces) interface = Interface(actionsmap=actionsmap) interface.run(args, output_as=output_as, timeout=timeout) except MoulinetteError as e: diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index bccfdb9d..b36e1ece 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -402,18 +402,14 @@ class ActionsMap(object): all available namespaces. Keyword arguments: - - parser_class -- The BaseActionsMapParser derived class to use - for parsing the actions map + - top_parser -- A BaseActionsMapParser-derived instance to use for + parsing the actions map - namespaces -- The list of namespaces to use - - parser_kwargs -- A dict of arguments to pass to the parser - class at construction - """ - def __init__(self, parser_class, namespaces=[], parser_kwargs={}): - if not issubclass(parser_class, BaseActionsMapParser): - raise ValueError("Invalid parser class '%s'" % parser_class.__name__) - self.parser_class = parser_class + def __init__(self, top_parser, namespaces=[]): + + assert isinstance(top_parser, BaseActionsMapParser), "Invalid parser class '%s'" % top_parser.__class__.__name__ moulinette_env = init_moulinette_env() DATA_DIR = moulinette_env["DATA_DIR"] @@ -454,13 +450,8 @@ class ActionsMap(object): m18n.load_namespace(n) # Generate parsers - self.extraparser = ExtraArgumentParser(parser_class.interface) - self._parser = self._construct_parser(actionsmaps, **parser_kwargs) - - @property - def parser(self): - """Return the instance of the interface's actions map parser""" - return self._parser + self.extraparser = ExtraArgumentParser(top_parser.interface) + self.parser = self._construct_parser(actionsmaps, top_parser) def get_authenticator_for_profile(self, auth_profile): @@ -649,15 +640,15 @@ class ActionsMap(object): # Private methods - def _construct_parser(self, actionsmaps, **kwargs): + def _construct_parser(self, actionsmaps, top_parser): """ Construct the parser with the actions map Keyword arguments: - actionsmaps -- A dict of multi-level dictionnary of categories/actions/arguments list for each namespaces - - **kwargs -- Additionnal arguments to pass at the parser - class instantiation + - top_parser -- A BaseActionsMapParser-derived instance to use for + parsing the actions map Returns: An interface relevant's parser object @@ -668,13 +659,6 @@ class ActionsMap(object): # loaded ? Not sure about this ... old code is a bit mysterious... validate_extra = not self.from_cache - # Instantiate parser - # - # this either returns: - # * moulinette.interfaces.cli.ActionsMapParser - # * moulinette.interfaces.api.ActionsMapParser - top_parser = self.parser_class(**kwargs) - # namespace, actionsmap is a tuple where: # # * namespace define the top "name", for us it will always be From c750226a3b0516547ac9e8f4a34fab020ce402ad Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 03:48:01 +0200 Subject: [PATCH 08/14] We don't need no namespaces ... but let's it customizable through a var env if needed... --- moulinette/__init__.py | 23 +++++-------- moulinette/actionsmap.py | 72 +++++++++++++++++++--------------------- moulinette/globals.py | 1 + 3 files changed, 43 insertions(+), 53 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 0e9188ec..d9badd4c 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -72,13 +72,12 @@ def init(logging_config=None, **kwargs): # Easy access to interfaces -def api(namespaces, host="localhost", port=80, routes={}): +def api(host="localhost", port=80, routes={}): """Web server (API) interface Run a HTTP server with the moulinette for an API usage. Keyword arguments: - - namespaces -- The list of namespaces to use - host -- Server address to bind to - port -- Server port to bind to - routes -- A dict of additional routes to add in the form of @@ -88,31 +87,27 @@ def api(namespaces, host="localhost", port=80, routes={}): from moulinette.actionsmap import ActionsMap from moulinette.interfaces.api import Interface, ActionsMapParser try: - actionsmap = ActionsMap(ActionsMapParser(), - namespaces=namespaces) + actionsmap = ActionsMap(ActionsMapParser()) interface = Interface(actionsmap=actionsmap, routes=routes) interface.run(host, port) except MoulinetteError as e: import logging - - logging.getLogger(namespaces[0]).error(e.strerror) - return e.errno if hasattr(e, "errno") else 1 + logging.getLogger().error(e.strerror) + return 1 except KeyboardInterrupt: import logging - - logging.getLogger(namespaces[0]).info(m18n.g("operation_interrupted")) + logging.getLogger().info(m18n.g("operation_interrupted")) return 0 -def cli(namespaces, args, top_parser, output_as=None, timeout=None): +def cli(args, top_parser, output_as=None, timeout=None): """Command line interface Execute an action with the moulinette from the CLI and print its result in a readable format. Keyword arguments: - - namespaces -- The list of namespaces to use - args -- A list of argument strings - output_as -- Output result in another format, see moulinette.interfaces.cli.Interface for possible values @@ -122,14 +117,12 @@ def cli(namespaces, args, top_parser, output_as=None, timeout=None): from moulinette.actionsmap import ActionsMap from moulinette.interfaces.cli import Interface, ActionsMapParser try: - actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser), - namespaces=namespaces) + actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser)) interface = Interface(actionsmap=actionsmap) interface.run(args, output_as=output_as, timeout=timeout) except MoulinetteError as e: import logging - - logging.getLogger(namespaces[0]).error(e.strerror) + logging.getLogger().error(e.strerror) return 1 return 0 diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index b36e1ece..221a46fb 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -4,6 +4,7 @@ import os import re import logging import yaml +import glob import cPickle as pickle from time import time from collections import OrderedDict @@ -398,16 +399,14 @@ class ActionsMap(object): Moreover, the action can have specific argument(s). This class allows to manipulate one or several actions maps - associated to a namespace. If no namespace is given, it will load - all available namespaces. + associated to a namespace. Keyword arguments: - top_parser -- A BaseActionsMapParser-derived instance to use for parsing the actions map - - namespaces -- The list of namespaces to use """ - def __init__(self, top_parser, namespaces=[]): + def __init__(self, top_parser): assert isinstance(top_parser, BaseActionsMapParser), "Invalid parser class '%s'" % top_parser.__class__.__name__ @@ -415,12 +414,10 @@ class ActionsMap(object): DATA_DIR = moulinette_env["DATA_DIR"] CACHE_DIR = moulinette_env["CACHE_DIR"] - if len(namespaces) == 0: - namespaces = self.get_namespaces() actionsmaps = OrderedDict() # Iterate over actions map namespaces - for n in namespaces: + for n in self.get_namespaces(): logger.debug("loading actions map namespace '%s'", n) actionsmap_yml = "%s/actionsmap/%s.yml" % (DATA_DIR, n) @@ -441,10 +438,10 @@ class ActionsMap(object): # TODO: Switch to python3 and catch proper exception except (IOError, EOFError): self.from_cache = False - actionsmaps = self.generate_cache(namespaces) + actionsmaps[n] = self.generate_cache(n) else: # cache file doesn't exists self.from_cache = False - actionsmaps = self.generate_cache(namespaces) + actionsmaps[n] = self.generate_cache(n) # Load translations m18n.load_namespace(n) @@ -587,56 +584,55 @@ class ActionsMap(object): moulinette_env = init_moulinette_env() DATA_DIR = moulinette_env["DATA_DIR"] - for f in os.listdir("%s/actionsmap" % DATA_DIR): - if f.endswith(".yml"): - namespaces.append(f[:-4]) + # This var is ['*'] by default but could be set for example to + # ['yunohost', 'yml_*'] + NAMESPACE_PATTERNS = moulinette_env["NAMESPACES"] + + # Look for all files that match the given patterns in the actionsmap dir + for namespace_pattern in NAMESPACE_PATTERNS: + namespaces.extend(glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))) + + # Keep only the filenames with extension + namespaces = [os.path.basename(n)[:-4] for n in namespaces] + return namespaces @classmethod - def generate_cache(klass, namespaces=None): + def generate_cache(klass, namespace): """ Generate cache for the actions map's file(s) Keyword arguments: - - namespaces -- A list of namespaces to generate cache for + - namespace -- The namespace to generate cache for Returns: - A dict of actions map for each namespaces - + The action map for the namespace """ moulinette_env = init_moulinette_env() CACHE_DIR = moulinette_env["CACHE_DIR"] DATA_DIR = moulinette_env["DATA_DIR"] - actionsmaps = {} - if not namespaces: - namespaces = klass.get_namespaces() - # Iterate over actions map namespaces - for n in namespaces: - logger.debug("generating cache for actions map namespace '%s'", n) + logger.debug("generating cache for actions map namespace '%s'", namespace) - # Read actions map from yaml file - am_file = "%s/actionsmap/%s.yml" % (DATA_DIR, n) - with open(am_file, "r") as f: - actionsmaps[n] = ordered_yaml_load(f) + # Read actions map from yaml file + am_file = "%s/actionsmap/%s.yml" % (DATA_DIR, namespace) + with open(am_file, "r") as f: + actionsmap = ordered_yaml_load(f) - # at installation, cachedir might not exists - if os.path.exists("%s/actionsmap/" % CACHE_DIR): - # clean old cached files - for i in os.listdir("%s/actionsmap/" % CACHE_DIR): - if i.endswith(".pkl"): - os.remove("%s/actionsmap/%s" % (CACHE_DIR, i)) + # at installation, cachedir might not exists + for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, namespace)): + os.remove(old_cache) - # Cache actions map into pickle file - am_file_stat = os.stat(am_file) + # Cache actions map into pickle file + am_file_stat = os.stat(am_file) - pkl = "%s-%d-%d.pkl" % (n, am_file_stat.st_size, am_file_stat.st_mtime) + pkl = "%s-%d-%d.pkl" % (namespace, am_file_stat.st_size, am_file_stat.st_mtime) - with open_cachefile(pkl, "w", subdir="actionsmap") as f: - pickle.dump(actionsmaps[n], f) + with open_cachefile(pkl, "w", subdir="actionsmap") as f: + pickle.dump(actionsmap, f) - return actionsmaps + return actionsmap # Private methods diff --git a/moulinette/globals.py b/moulinette/globals.py index 39f45d93..025aab52 100644 --- a/moulinette/globals.py +++ b/moulinette/globals.py @@ -11,4 +11,5 @@ def init_moulinette_env(): "MOULINETTE_LOCALES_DIR", "/usr/share/moulinette/locale" ), "CACHE_DIR": environ.get("MOULINETTE_CACHE_DIR", "/var/cache/moulinette"), + "NAMESPACES": environ.get("MOULINETTE_NAMESPACES", "*").split(), # By default we'll load every namespace we find } From 559f40a4eaf8aad996fd295ad48a2008828c5843 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 03:58:37 +0200 Subject: [PATCH 09/14] Another round of simplification for interface init... --- moulinette/__init__.py | 15 ++++----------- moulinette/interfaces/api.py | 6 ++++-- moulinette/interfaces/cli.py | 6 ++++-- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index d9badd4c..3a80381d 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -84,13 +84,9 @@ def api(host="localhost", port=80, routes={}): {(method, uri): callback} """ - from moulinette.actionsmap import ActionsMap - from moulinette.interfaces.api import Interface, ActionsMapParser + from moulinette.interfaces.api import Interface as Api try: - actionsmap = ActionsMap(ActionsMapParser()) - interface = Interface(actionsmap=actionsmap, - routes=routes) - interface.run(host, port) + Api(routes=routes).run(host, port) except MoulinetteError as e: import logging logging.getLogger().error(e.strerror) @@ -114,12 +110,9 @@ def cli(args, top_parser, output_as=None, timeout=None): - top_parser -- The top parser used to build the ActionsMapParser """ - from moulinette.actionsmap import ActionsMap - from moulinette.interfaces.cli import Interface, ActionsMapParser + from moulinette.interfaces.cli import Interface as Cli try: - actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser)) - interface = Interface(actionsmap=actionsmap) - interface.run(args, output_as=output_as, timeout=timeout) + Cli(top_parser=top_parser).run(args, output_as=output_as, timeout=timeout) except MoulinetteError as e: import logging logging.getLogger().error(e.strerror) diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index 084e5671..f251c215 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -14,6 +14,7 @@ from bottle import request, response, Bottle, HTTPResponse from bottle import abort from moulinette import msignals, m18n, env +from moulinette.actionsmap import ActionsMap from moulinette.core import MoulinetteError from moulinette.interfaces import ( BaseActionsMapParser, @@ -729,7 +730,6 @@ class Interface(BaseInterface): actions map. Keyword arguments: - - actionsmap -- The ActionsMap instance to connect to - routes -- A dict of additional routes to add in the form of {(method, path): callback} - log_queues -- A LogQueues object or None to retrieve it from @@ -737,7 +737,9 @@ class Interface(BaseInterface): """ - def __init__(self, actionsmap, routes={}, log_queues=None): + def __init__(self, routes={}, log_queues=None): + + actionsmap = ActionsMap(ActionsMapParser()) # Attempt to retrieve log queues from an APIQueueHandler if log_queues is None: diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index 5c5e4f60..f1e8f834 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -12,6 +12,7 @@ from datetime import date, datetime import argcomplete from moulinette import msignals, m18n +from moulinette.actionsmap import ActionsMap from moulinette.core import MoulinetteError from moulinette.interfaces import ( BaseActionsMapParser, @@ -419,7 +420,8 @@ class Interface(BaseInterface): """ - def __init__(self, actionsmap): + def __init__(self, top_parser=None): + # Set user locale m18n.set_locale(get_locale()) @@ -429,7 +431,7 @@ class Interface(BaseInterface): msignals.set_handler("authenticate", self._do_authenticate) msignals.set_handler("prompt", self._do_prompt) - self.actionsmap = actionsmap + self.actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser)) def run(self, args, output_as=None, timeout=None): """Run the moulinette From 89ad543797c1c7e3f7eca3a51df22f8582a72865 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 04:33:14 +0200 Subject: [PATCH 10/14] Tweaking debug messages --- moulinette/actionsmap.py | 3 ++- moulinette/interfaces/__init__.py | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index 221a46fb..49f46565 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -283,7 +283,6 @@ class ExtraArgumentParser(object): if iface in klass.skipped_iface: continue self.extra[klass.name] = klass - logger.debug("extra parameter classes loaded: %s", self.extra.keys()) def validate(self, arg_name, parameters): """ @@ -651,6 +650,8 @@ class ActionsMap(object): """ + logger.debug("building parser...") + # If loading from cache, extra were already checked when cache was # loaded ? Not sure about this ... old code is a bit mysterious... validate_extra = not self.from_cache diff --git a/moulinette/interfaces/__init__.py b/moulinette/interfaces/__init__.py index 4f2e33dd..efc21db9 100644 --- a/moulinette/interfaces/__init__.py +++ b/moulinette/interfaces/__init__.py @@ -342,11 +342,6 @@ class _CallbackAction(argparse.Action): self.callback_method = callback.get("method") self.callback_kwargs = callback.get("kwargs", {}) self.callback_return = callback.get("return", False) - logger.debug( - "registering new callback action '{0}' to {1}".format( - self.callback_method, option_strings - ) - ) @property def callback(self): From 677f4518e4c790510f08989be0f5500bd3f43a2d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 05:43:32 +0200 Subject: [PATCH 11/14] Ugly hack to only load 1 category to speed up execution time --- moulinette/__init__.py | 3 ++- moulinette/actionsmap.py | 15 ++++++++++++++- moulinette/interfaces/cli.py | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index 3a80381d..d27acac6 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -112,7 +112,8 @@ def cli(args, top_parser, output_as=None, timeout=None): """ from moulinette.interfaces.cli import Interface as Cli try: - Cli(top_parser=top_parser).run(args, output_as=output_as, timeout=timeout) + load_only_category = args[0] if args and not args[0].startswith("-") else None + Cli(top_parser=top_parser, load_only_category=load_only_category).run(args, output_as=output_as, timeout=timeout) except MoulinetteError as e: import logging logging.getLogger().error(e.strerror) diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index 49f46565..4329160e 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -403,9 +403,13 @@ class ActionsMap(object): Keyword arguments: - top_parser -- A BaseActionsMapParser-derived instance to use for parsing the actions map + - load_only_category -- A name of a category that should only be the + one loaded because it's been already determined + that's the only one relevant ... used for optimization + purposes... """ - def __init__(self, top_parser): + def __init__(self, top_parser, load_only_category=None): assert isinstance(top_parser, BaseActionsMapParser), "Invalid parser class '%s'" % top_parser.__class__.__name__ @@ -442,6 +446,13 @@ class ActionsMap(object): self.from_cache = False actionsmaps[n] = self.generate_cache(n) + # If load_only_category is set, and *if* the target category + # is in the actionsmap, we'll load only that one. + # If we filter it even if it doesn't exist, we'll end up with a + # weird help message when we do a typo in the category name.. + if load_only_category and load_only_category in actionsmaps[n]: + actionsmaps[n] = {k: v for k, v in actionsmaps[n].items() if k in [load_only_category, "_global"]} + # Load translations m18n.load_namespace(n) @@ -651,6 +662,7 @@ class ActionsMap(object): """ logger.debug("building parser...") + start = time() # If loading from cache, extra were already checked when cache was # loaded ? Not sure about this ... old code is a bit mysterious... @@ -756,4 +768,5 @@ class ActionsMap(object): tid, action_options["configuration"] ) + logger.debug("building parser took %.3fs", time() - start) return top_parser diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index f1e8f834..186c0c89 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -420,7 +420,7 @@ class Interface(BaseInterface): """ - def __init__(self, top_parser=None): + def __init__(self, top_parser=None, load_only_category=None): # Set user locale m18n.set_locale(get_locale()) @@ -431,7 +431,7 @@ class Interface(BaseInterface): msignals.set_handler("authenticate", self._do_authenticate) msignals.set_handler("prompt", self._do_prompt) - self.actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser)) + self.actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser), load_only_category=load_only_category) def run(self, args, output_as=None, timeout=None): """Run the moulinette From a2a6e6fe7c1fc370e3fb4aa79afee67345860a50 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 May 2020 06:11:14 +0200 Subject: [PATCH 12/14] Attempt to fix the tests :s --- test/conftest.py | 21 +++++---------------- test/test_actionsmap.py | 12 ++++-------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 59c7f832..3f400b17 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -125,13 +125,8 @@ def moulinette_webapi(moulinette): CookiePolicy.return_ok_secure = return_true - moulinette_webapi = moulinette.init_interface( - "api", - kwargs={"routes": {}, "use_websocket": False}, - actionsmap={"namespaces": ["moulitest"], "use_cache": True}, - ) - - return TestApp(moulinette_webapi._app) + from moulinette.interfaces.api import Interface as Api + return TestApp(Api(routes={})._app) @pytest.fixture @@ -148,17 +143,11 @@ def moulinette_cli(moulinette, mocker): help="Log and print debug messages", ) mocker.patch("os.isatty", return_value=True) - moulinette_cli = moulinette.init_interface( - "cli", - actionsmap={ - "namespaces": ["moulitest"], - "use_cache": False, - "parser_kwargs": {"top_parser": parser}, - }, - ) + from moulinette.interfaces.cli import Interface as Cli + cli = Cli(top_parser=parser) mocker.stopall() - return moulinette_cli + return cli @pytest.fixture diff --git a/test/test_actionsmap.py b/test/test_actionsmap.py index b69b5179..de2942fe 100644 --- a/test/test_actionsmap.py +++ b/test/test_actionsmap.py @@ -225,7 +225,7 @@ def test_extra_argument_parser_parse_args(iface, mocker): def test_actions_map_api(): from moulinette.interfaces.api import ActionsMapParser - amap = ActionsMap(ActionsMapParser, use_cache=False) + amap = ActionsMap(ActionsMapParser()) assert amap.parser.global_conf["authenticate"] == "all" assert "default" in amap.parser.global_conf["authenticator"] @@ -235,7 +235,7 @@ def test_actions_map_api(): amap.generate_cache() - amap = ActionsMap(ActionsMapParser, use_cache=True) + amap = ActionsMap(ActionsMapParser()) assert amap.parser.global_conf["authenticate"] == "all" assert "default" in amap.parser.global_conf["authenticator"] @@ -274,9 +274,7 @@ def test_actions_map_cli(): default=False, help="Log and print debug messages", ) - amap = ActionsMap( - ActionsMapParser, use_cache=False, parser_kwargs={"top_parser": parser} - ) + amap = ActionsMap(ActionsMapParser(top_parser=parser)) assert amap.parser.global_conf["authenticate"] == "all" assert "default" in amap.parser.global_conf["authenticator"] @@ -295,9 +293,7 @@ def test_actions_map_cli(): amap.generate_cache() - amap = ActionsMap( - ActionsMapParser, use_cache=True, parser_kwargs={"top_parser": parser} - ) + amap = ActionsMap(ActionsMapParser(top_parser=parser)) assert amap.parser.global_conf["authenticate"] == "all" assert "default" in amap.parser.global_conf["authenticator"] From e8309384e565cb0bb6081d3621b79b5bb94c5e74 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 1 May 2020 14:00:10 +0200 Subject: [PATCH 13/14] fix tests --- moulinette/actionsmap.py | 6 +++--- test/test_actionsmap.py | 10 +++++----- test/test_auth.py | 14 +++++--------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index 4329160e..2fe17d71 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -419,6 +419,7 @@ class ActionsMap(object): actionsmaps = OrderedDict() + self.from_cache = False # Iterate over actions map namespaces for n in self.get_namespaces(): logger.debug("loading actions map namespace '%s'", n) @@ -433,17 +434,16 @@ class ActionsMap(object): ) if os.path.exists(actionsmap_pkl): - self.from_cache = True try: # Attempt to load cache with open(actionsmap_pkl) as f: actionsmaps[n] = pickle.load(f) + + self.from_cache = True # TODO: Switch to python3 and catch proper exception except (IOError, EOFError): - self.from_cache = False actionsmaps[n] = self.generate_cache(n) else: # cache file doesn't exists - self.from_cache = False actionsmaps[n] = self.generate_cache(n) # If load_only_category is set, and *if* the target category diff --git a/test/test_actionsmap.py b/test/test_actionsmap.py index de2942fe..69af0472 100644 --- a/test/test_actionsmap.py +++ b/test/test_actionsmap.py @@ -158,10 +158,10 @@ def test_required_paremeter_missing_value(iface, caplog): def test_actions_map_unknown_authenticator(monkeypatch, tmp_path): monkeypatch.setenv("MOULINETTE_DATA_DIR", str(tmp_path)) - actionsmap_dir = actionsmap_dir = tmp_path / "actionsmap" + actionsmap_dir = tmp_path / "actionsmap" actionsmap_dir.mkdir() - amap = ActionsMap(BaseActionsMapParser) + amap = ActionsMap(BaseActionsMapParser()) with pytest.raises(ValueError) as exception: amap.get_authenticator_for_profile("unknown") assert "Unknown authenticator" in str(exception) @@ -233,7 +233,7 @@ def test_actions_map_api(): assert ("GET", "/test-auth/default") in amap.parser.routes assert ("POST", "/test-auth/subcat/post") in amap.parser.routes - amap.generate_cache() + amap.generate_cache("moulitest") amap = ActionsMap(ActionsMapParser()) @@ -247,7 +247,7 @@ def test_actions_map_api(): def test_actions_map_import_error(mocker): from moulinette.interfaces.api import ActionsMapParser - amap = ActionsMap(ActionsMapParser) + amap = ActionsMap(ActionsMapParser()) from moulinette.core import MoulinetteLock @@ -291,7 +291,7 @@ def test_actions_map_cli(): .choices ) - amap.generate_cache() + amap.generate_cache("moulitest") amap = ActionsMap(ActionsMapParser(top_parser=parser)) diff --git a/test/test_auth.py b/test/test_auth.py index dd95d9c7..a7a79c90 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -216,18 +216,15 @@ class TestAuthCLI: assert "some_data_from_default" in message.out - moulinette_cli.run( - ["testauth", "default"], output_as="plain", password="default" - ) + moulinette_cli.run(["testauth", "default"], output_as="plain") message = capsys.readouterr() assert "some_data_from_default" in message.out def test_login_bad_password(self, moulinette_cli, capsys, mocker): + mocker.patch("getpass.getpass", return_value="Bad Password") with pytest.raises(MoulinetteError): - moulinette_cli.run( - ["testauth", "default"], output_as="plain", password="Bad Password" - ) + moulinette_cli.run(["testauth", "default"], output_as="plain") mocker.patch("getpass.getpass", return_value="Bad Password") with pytest.raises(MoulinetteError): @@ -242,10 +239,9 @@ class TestAuthCLI: expected_msg = translation.format() assert expected_msg in str(exception) + mocker.patch("getpass.getpass", return_value="yoloswag") with pytest.raises(MoulinetteError) as exception: - moulinette_cli.run( - ["testauth", "default"], output_as="none", password="yoloswag" - ) + moulinette_cli.run(["testauth", "default"], output_as="none") expected_msg = translation.format() assert expected_msg in str(exception) From 708b0330d2c4a0acf0343f7c3ccbd400609b9810 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 1 May 2020 14:03:23 +0200 Subject: [PATCH 14/14] black/flake8 --- moulinette/__init__.py | 9 ++++++++- moulinette/actionsmap.py | 14 +++++++++++--- moulinette/globals.py | 4 +++- moulinette/interfaces/api.py | 13 +++---------- moulinette/interfaces/cli.py | 5 ++++- test/conftest.py | 2 ++ 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/moulinette/__init__.py b/moulinette/__init__.py index d27acac6..cf1992b8 100755 --- a/moulinette/__init__.py +++ b/moulinette/__init__.py @@ -85,14 +85,17 @@ def api(host="localhost", port=80, routes={}): """ from moulinette.interfaces.api import Interface as Api + try: Api(routes=routes).run(host, port) except MoulinetteError as e: import logging + logging.getLogger().error(e.strerror) return 1 except KeyboardInterrupt: import logging + logging.getLogger().info(m18n.g("operation_interrupted")) return 0 @@ -111,11 +114,15 @@ def cli(args, top_parser, output_as=None, timeout=None): """ from moulinette.interfaces.cli import Interface as Cli + try: load_only_category = args[0] if args and not args[0].startswith("-") else None - Cli(top_parser=top_parser, load_only_category=load_only_category).run(args, output_as=output_as, timeout=timeout) + Cli(top_parser=top_parser, load_only_category=load_only_category).run( + args, output_as=output_as, timeout=timeout + ) except MoulinetteError as e: import logging + logging.getLogger().error(e.strerror) return 1 return 0 diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index 2fe17d71..7a365469 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -411,7 +411,9 @@ class ActionsMap(object): def __init__(self, top_parser, load_only_category=None): - assert isinstance(top_parser, BaseActionsMapParser), "Invalid parser class '%s'" % top_parser.__class__.__name__ + assert isinstance(top_parser, BaseActionsMapParser), ( + "Invalid parser class '%s'" % top_parser.__class__.__name__ + ) moulinette_env = init_moulinette_env() DATA_DIR = moulinette_env["DATA_DIR"] @@ -451,7 +453,11 @@ class ActionsMap(object): # If we filter it even if it doesn't exist, we'll end up with a # weird help message when we do a typo in the category name.. if load_only_category and load_only_category in actionsmaps[n]: - actionsmaps[n] = {k: v for k, v in actionsmaps[n].items() if k in [load_only_category, "_global"]} + actionsmaps[n] = { + k: v + for k, v in actionsmaps[n].items() + if k in [load_only_category, "_global"] + } # Load translations m18n.load_namespace(n) @@ -600,7 +606,9 @@ class ActionsMap(object): # Look for all files that match the given patterns in the actionsmap dir for namespace_pattern in NAMESPACE_PATTERNS: - namespaces.extend(glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))) + namespaces.extend( + glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern)) + ) # Keep only the filenames with extension namespaces = [os.path.basename(n)[:-4] for n in namespaces] diff --git a/moulinette/globals.py b/moulinette/globals.py index 025aab52..8a169cea 100644 --- a/moulinette/globals.py +++ b/moulinette/globals.py @@ -11,5 +11,7 @@ def init_moulinette_env(): "MOULINETTE_LOCALES_DIR", "/usr/share/moulinette/locale" ), "CACHE_DIR": environ.get("MOULINETTE_CACHE_DIR", "/var/cache/moulinette"), - "NAMESPACES": environ.get("MOULINETTE_NAMESPACES", "*").split(), # By default we'll load every namespace we find + "NAMESPACES": environ.get( + "MOULINETTE_NAMESPACES", "*" + ).split(), # By default we'll load every namespace we find } diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index f251c215..4714dd09 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -288,10 +288,7 @@ class _ActionsMapPlugin(object): # Append messages route app.route( - "/messages", - name="messages", - callback=self.messages, - skip=["actionsmap"], + "/messages", name="messages", callback=self.messages, skip=["actionsmap"], ) # Append routes from the actions map @@ -796,18 +793,14 @@ class Interface(BaseInterface): """ logger.debug( - "starting the server instance in %s:%d", - host, - port, + "starting the server instance in %s:%d", host, port, ) try: from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler - server = WSGIServer( - (host, port), self._app, handler_class=WebSocketHandler - ) + server = WSGIServer((host, port), self._app, handler_class=WebSocketHandler) server.serve_forever() except IOError as e: logger.exception("unable to start the server instance on %s:%d", host, port) diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index 186c0c89..d8e2dd2b 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -431,7 +431,10 @@ class Interface(BaseInterface): msignals.set_handler("authenticate", self._do_authenticate) msignals.set_handler("prompt", self._do_prompt) - self.actionsmap = ActionsMap(ActionsMapParser(top_parser=top_parser), load_only_category=load_only_category) + self.actionsmap = ActionsMap( + ActionsMapParser(top_parser=top_parser), + load_only_category=load_only_category, + ) def run(self, args, output_as=None, timeout=None): """Run the moulinette diff --git a/test/conftest.py b/test/conftest.py index 3f400b17..8e0ec8ac 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -126,6 +126,7 @@ def moulinette_webapi(moulinette): CookiePolicy.return_ok_secure = return_true from moulinette.interfaces.api import Interface as Api + return TestApp(Api(routes={})._app) @@ -144,6 +145,7 @@ def moulinette_cli(moulinette, mocker): ) mocker.patch("os.isatty", return_value=True) from moulinette.interfaces.cli import Interface as Cli + cli = Cli(top_parser=parser) mocker.stopall()