Use black to format source code

This commit is contained in:
Luke Murphy 2019-09-13 15:26:04 +02:00
parent 6829a3dc7e
commit 7c3517e730
No known key found for this signature in database
GPG key ID: 5E2EF5A63E3718CC
25 changed files with 715 additions and 405 deletions

View file

@ -6,6 +6,8 @@ matrix:
env: TOXENV=py27 env: TOXENV=py27
- python: 2.7 - python: 2.7
env: TOXENV=lint env: TOXENV=lint
- python: 3.6
env: TOXENV=format-check
- python: 2.7 - python: 2.7
env: TOXENV=docs env: TOXENV=docs

View file

@ -1,5 +1,6 @@
[![Build Status](https://travis-ci.org/YunoHost/moulinette.svg?branch=stretch-unstable)](https://travis-ci.org/YunoHost/moulinette) [![Build Status](https://travis-ci.org/YunoHost/moulinette.svg?branch=stretch-unstable)](https://travis-ci.org/YunoHost/moulinette)
[![GitHub license](https://img.shields.io/github/license/YunoHost/moulinette)](https://github.com/YunoHost/moulinette/blob/stretch-unstable/LICENSE) [![GitHub license](https://img.shields.io/github/license/YunoHost/moulinette)](https://github.com/YunoHost/moulinette/blob/stretch-unstable/LICENSE)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
Moulinette Moulinette
========== ==========
@ -61,3 +62,10 @@ Testing
$ pip install tox $ pip install tox
$ tox $ tox
``` ```
A note regarding the use of [Black](https://github.com/psf/black) for source
code formatting. The actual source code of Moulinette is still written using
Python 2. Black can still format this code but it must within a Python 3
environment. Therefore, you'll need to manage this environment switching when
you invoke Black through Tox (`tox -e format`). An environment created with
your system Python 3 should suffice (`python3 -m venv .venv` etc.).

View file

@ -1,15 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from moulinette.core import init_interface, MoulinetteError, MoulinetteSignals, Moulinette18n from moulinette.core import (
init_interface,
MoulinetteError,
MoulinetteSignals,
Moulinette18n,
)
from moulinette.globals import init_moulinette_env from moulinette.globals import init_moulinette_env
__title__ = 'moulinette' __title__ = 'moulinette'
__version__ = '0.1' __version__ = '0.1'
__author__ = ['Kload', __author__ = ['Kload', 'jlebleu', 'titoko', 'beudbeud', 'npze']
'jlebleu',
'titoko',
'beudbeud',
'npze']
__license__ = 'AGPL 3.0' __license__ = 'AGPL 3.0'
__credits__ = """ __credits__ = """
Copyright (C) 2014 YUNOHOST.ORG Copyright (C) 2014 YUNOHOST.ORG
@ -27,10 +28,7 @@ __credits__ = """
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses along with this program; if not, see http://www.gnu.org/licenses
""" """
__all__ = [ __all__ = ['init', 'api', 'cli', 'm18n', 'env', 'init_interface', 'MoulinetteError']
'init', 'api', 'cli', 'm18n', 'env',
'init_interface', 'MoulinetteError',
]
msignals = MoulinetteSignals() msignals = MoulinetteSignals()
@ -40,6 +38,7 @@ m18n = Moulinette18n()
# Package functions # Package functions
def init(logging_config=None, **kwargs): def init(logging_config=None, **kwargs):
"""Package initialization """Package initialization
@ -66,8 +65,10 @@ def init(logging_config=None, **kwargs):
# Easy access to interfaces # Easy access to interfaces
def api(namespaces, host='localhost', port=80, routes={},
use_websocket=True, use_cache=True): def api(
namespaces, host='localhost', port=80, routes={}, use_websocket=True, use_cache=True
):
"""Web server (API) interface """Web server (API) interface
Run a HTTP server with the moulinette for an API usage. Run a HTTP server with the moulinette for an API usage.
@ -84,29 +85,33 @@ def api(namespaces, host='localhost', port=80, routes={},
""" """
try: try:
moulinette = init_interface('api', moulinette = init_interface(
kwargs={ 'api',
'routes': routes, kwargs={'routes': routes, 'use_websocket': use_websocket},
'use_websocket': use_websocket actionsmap={'namespaces': namespaces, 'use_cache': use_cache},
},
actionsmap={
'namespaces': namespaces,
'use_cache': use_cache
}
) )
moulinette.run(host, port) moulinette.run(host, port)
except MoulinetteError as e: except MoulinetteError as e:
import logging import logging
logging.getLogger(namespaces[0]).error(e.strerror) logging.getLogger(namespaces[0]).error(e.strerror)
return e.errno if hasattr(e, "errno") else 1 return e.errno if hasattr(e, "errno") else 1
except KeyboardInterrupt: except KeyboardInterrupt:
import logging import logging
logging.getLogger(namespaces[0]).info(m18n.g('operation_interrupted')) logging.getLogger(namespaces[0]).info(m18n.g('operation_interrupted'))
return 0 return 0
def cli(namespaces, args, use_cache=True, output_as=None, def cli(
password=None, timeout=None, parser_kwargs={}): namespaces,
args,
use_cache=True,
output_as=None,
password=None,
timeout=None,
parser_kwargs={},
):
"""Command line interface """Command line interface
Execute an action with the moulinette from the CLI and print its Execute an action with the moulinette from the CLI and print its
@ -125,7 +130,8 @@ def cli(namespaces, args, use_cache=True, output_as=None,
""" """
try: try:
moulinette = init_interface('cli', moulinette = init_interface(
'cli',
actionsmap={ actionsmap={
'namespaces': namespaces, 'namespaces': namespaces,
'use_cache': use_cache, 'use_cache': use_cache,
@ -135,6 +141,7 @@ def cli(namespaces, args, use_cache=True, output_as=None,
moulinette.run(args, output_as=output_as, password=password, timeout=timeout) moulinette.run(args, output_as=output_as, password=password, timeout=timeout)
except MoulinetteError as e: except MoulinetteError as e:
import logging import logging
logging.getLogger(namespaces[0]).error(e.strerror) logging.getLogger(namespaces[0]).error(e.strerror)
return 1 return 1
return 0 return 0

View file

@ -11,10 +11,8 @@ from collections import OrderedDict
from moulinette import m18n, msignals from moulinette import m18n, msignals
from moulinette.cache import open_cachefile from moulinette.cache import open_cachefile
from moulinette.globals import init_moulinette_env from moulinette.globals import init_moulinette_env
from moulinette.core import (MoulinetteError, MoulinetteLock) from moulinette.core import MoulinetteError, MoulinetteLock
from moulinette.interfaces import ( from moulinette.interfaces import BaseActionsMapParser, GLOBAL_SECTION, TO_RETURN_PROP
BaseActionsMapParser, GLOBAL_SECTION, TO_RETURN_PROP
)
from moulinette.utils.log import start_action_logging from moulinette.utils.log import start_action_logging
logger = logging.getLogger('moulinette.actionsmap') logger = logging.getLogger('moulinette.actionsmap')
@ -24,6 +22,7 @@ logger = logging.getLogger('moulinette.actionsmap')
# Extra parameters definition # Extra parameters definition
class _ExtraParameter(object): class _ExtraParameter(object):
""" """
@ -95,12 +94,15 @@ class CommentParameter(_ExtraParameter):
def validate(klass, value, arg_name): def validate(klass, value, arg_name):
# Deprecated boolean or empty string # Deprecated boolean or empty string
if isinstance(value, bool) or (isinstance(value, str) and not value): if isinstance(value, bool) or (isinstance(value, str) and not value):
logger.warning("expecting a non-empty string for extra parameter '%s' of " logger.warning(
"argument '%s'", klass.name, arg_name) "expecting a non-empty string for extra parameter '%s' of "
"argument '%s'",
klass.name,
arg_name,
)
value = arg_name value = arg_name
elif not isinstance(value, str): elif not isinstance(value, str):
raise TypeError("parameter value must be a string, got %r" raise TypeError("parameter value must be a string, got %r" % value)
% value)
return value return value
@ -113,6 +115,7 @@ class AskParameter(_ExtraParameter):
when asking the argument value. when asking the argument value.
""" """
name = 'ask' name = 'ask'
skipped_iface = ['api'] skipped_iface = ['api']
@ -130,12 +133,15 @@ class AskParameter(_ExtraParameter):
def validate(klass, value, arg_name): def validate(klass, value, arg_name):
# Deprecated boolean or empty string # Deprecated boolean or empty string
if isinstance(value, bool) or (isinstance(value, str) and not value): if isinstance(value, bool) or (isinstance(value, str) and not value):
logger.warning("expecting a non-empty string for extra parameter '%s' of " logger.warning(
"argument '%s'", klass.name, arg_name) "expecting a non-empty string for extra parameter '%s' of "
"argument '%s'",
klass.name,
arg_name,
)
value = arg_name value = arg_name
elif not isinstance(value, str): elif not isinstance(value, str):
raise TypeError("parameter value must be a string, got %r" raise TypeError("parameter value must be a string, got %r" % value)
% value)
return value return value
@ -148,6 +154,7 @@ class PasswordParameter(AskParameter):
when asking the password. when asking the password.
""" """
name = 'password' name = 'password'
def __call__(self, message, arg_name, arg_value): def __call__(self, message, arg_name, arg_value):
@ -170,6 +177,7 @@ class PatternParameter(_ExtraParameter):
the message to display if it doesn't match. the message to display if it doesn't match.
""" """
name = 'pattern' name = 'pattern'
def __call__(self, arguments, arg_name, arg_value): def __call__(self, arguments, arg_name, arg_value):
@ -182,28 +190,32 @@ class PatternParameter(_ExtraParameter):
v = arg_value v = arg_value
if v and not re.match(pattern, v or '', re.UNICODE): if v and not re.match(pattern, v or '', re.UNICODE):
logger.debug("argument value '%s' for '%s' doesn't match pattern '%s'", logger.debug(
v, arg_name, pattern) "argument value '%s' for '%s' doesn't match pattern '%s'",
v,
arg_name,
pattern,
)
# Attempt to retrieve message translation # Attempt to retrieve message translation
msg = m18n.n(message) msg = m18n.n(message)
if msg == message: if msg == message:
msg = m18n.g(message) msg = m18n.g(message)
raise MoulinetteError('invalid_argument', raise MoulinetteError('invalid_argument', argument=arg_name, error=msg)
argument=arg_name, error=msg)
return arg_value return arg_value
@staticmethod @staticmethod
def validate(value, arg_name): def validate(value, arg_name):
# Deprecated string type # Deprecated string type
if isinstance(value, str): if isinstance(value, str):
logger.warning("expecting a list as extra parameter 'pattern' of " logger.warning(
"argument '%s'", arg_name) "expecting a list as extra parameter 'pattern' of " "argument '%s'",
arg_name,
)
value = [value, 'pattern_not_match'] value = [value, 'pattern_not_match']
elif not isinstance(value, list) or len(value) != 2: elif not isinstance(value, list) or len(value) != 2:
raise TypeError("parameter value must be a list, got %r" raise TypeError("parameter value must be a list, got %r" % value)
% value)
return value return value
@ -215,21 +227,19 @@ class RequiredParameter(_ExtraParameter):
The value of this parameter must be a boolean which is set to False by The value of this parameter must be a boolean which is set to False by
default. default.
""" """
name = 'required' name = 'required'
def __call__(self, required, arg_name, arg_value): def __call__(self, required, arg_name, arg_value):
if required and (arg_value is None or arg_value == ''): if required and (arg_value is None or arg_value == ''):
logger.debug("argument '%s' is required", logger.debug("argument '%s' is required", arg_name)
arg_name) raise MoulinetteError('argument_required', argument=arg_name)
raise MoulinetteError('argument_required',
argument=arg_name)
return arg_value return arg_value
@staticmethod @staticmethod
def validate(value, arg_name): def validate(value, arg_name):
if not isinstance(value, bool): if not isinstance(value, bool):
raise TypeError("parameter value must be a list, got %r" raise TypeError("parameter value must be a list, got %r" % value)
% value)
return value return value
@ -238,8 +248,13 @@ The list of available extra parameters classes. It will keep to this list
order on argument parsing. order on argument parsing.
""" """
extraparameters_list = [CommentParameter, AskParameter, PasswordParameter, extraparameters_list = [
RequiredParameter, PatternParameter] CommentParameter,
AskParameter,
PasswordParameter,
RequiredParameter,
PatternParameter,
]
# Extra parameters argument Parser # Extra parameters argument Parser
@ -286,8 +301,13 @@ class ExtraArgumentParser(object):
# Validate parameter value # Validate parameter value
parameters[p] = klass.validate(v, arg_name) parameters[p] = klass.validate(v, arg_name)
except Exception as e: except Exception as e:
logger.error("unable to validate extra parameter '%s' " logger.error(
"for argument '%s': %s", p, arg_name, e) "unable to validate extra parameter '%s' "
"for argument '%s': %s",
p,
arg_name,
e,
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
return parameters return parameters
@ -353,12 +373,15 @@ class ExtraArgumentParser(object):
# Main class ---------------------------------------------------------- # Main class ----------------------------------------------------------
def ordered_yaml_load(stream): def ordered_yaml_load(stream):
class OrderedLoader(yaml.Loader): class OrderedLoader(yaml.Loader):
pass pass
OrderedLoader.add_constructor( OrderedLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
lambda loader, node: OrderedDict(loader.construct_pairs(node))) lambda loader, node: OrderedDict(loader.construct_pairs(node)),
)
return yaml.load(stream, OrderedLoader) return yaml.load(stream, OrderedLoader)
@ -386,8 +409,7 @@ class ActionsMap(object):
""" """
def __init__(self, parser_class, namespaces=[], use_cache=True, def __init__(self, parser_class, namespaces=[], use_cache=True, parser_kwargs={}):
parser_kwargs={}):
if not issubclass(parser_class, BaseActionsMapParser): if not issubclass(parser_class, BaseActionsMapParser):
raise ValueError("Invalid parser class '%s'" % parser_class.__name__) raise ValueError("Invalid parser class '%s'" % parser_class.__name__)
self.parser_class = parser_class self.parser_class = parser_class
@ -411,7 +433,7 @@ class ActionsMap(object):
CACHE_DIR, CACHE_DIR,
n, n,
actionsmap_yml_stat.st_size, actionsmap_yml_stat.st_size,
actionsmap_yml_stat.st_mtime actionsmap_yml_stat.st_mtime,
) )
if use_cache and os.path.exists(actionsmap_pkl): if use_cache and os.path.exists(actionsmap_pkl):
@ -487,8 +509,17 @@ class ActionsMap(object):
# Retrieve action information # Retrieve action information
if len(tid) == 4: if len(tid) == 4:
namespace, category, subcategory, action = tid namespace, category, subcategory, action = tid
func_name = '%s_%s_%s' % (category, subcategory.replace('-', '_'), action.replace('-', '_')) func_name = '%s_%s_%s' % (
full_action_name = "%s.%s.%s.%s" % (namespace, category, subcategory, action) category,
subcategory.replace('-', '_'),
action.replace('-', '_'),
)
full_action_name = "%s.%s.%s.%s" % (
namespace,
category,
subcategory,
action,
)
else: else:
assert len(tid) == 3 assert len(tid) == 3
namespace, category, action = tid namespace, category, action = tid
@ -500,25 +531,33 @@ class ActionsMap(object):
with MoulinetteLock(namespace, timeout): with MoulinetteLock(namespace, timeout):
start = time() start = time()
try: try:
mod = __import__('%s.%s' % (namespace, category), mod = __import__(
globals=globals(), level=0, '%s.%s' % (namespace, category),
fromlist=[func_name]) globals=globals(),
logger.debug('loading python module %s took %.3fs', level=0,
'%s.%s' % (namespace, category), time() - start) fromlist=[func_name],
)
logger.debug(
'loading python module %s took %.3fs',
'%s.%s' % (namespace, category),
time() - start,
)
func = getattr(mod, func_name) func = getattr(mod, func_name)
except (AttributeError, ImportError): except (AttributeError, ImportError):
logger.exception("unable to load function %s.%s", logger.exception("unable to load function %s.%s", namespace, func_name)
namespace, func_name)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
else: else:
log_id = start_action_logging() log_id = start_action_logging()
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
# Log arguments in debug mode only for safety reasons # Log arguments in debug mode only for safety reasons
logger.info('processing action [%s]: %s with args=%s', logger.info(
log_id, full_action_name, arguments) 'processing action [%s]: %s with args=%s',
log_id,
full_action_name,
arguments,
)
else: else:
logger.info('processing action [%s]: %s', logger.info('processing action [%s]: %s', log_id, full_action_name)
log_id, full_action_name)
# Load translation and process the action # Load translation and process the action
m18n.load_namespace(namespace) m18n.load_namespace(namespace)
@ -527,8 +566,7 @@ class ActionsMap(object):
return func(**arguments) return func(**arguments)
finally: finally:
stop = time() stop = time()
logger.debug('action [%s] executed in %.3fs', logger.debug('action [%s] executed in %.3fs', log_id, stop - start)
log_id, stop - start)
@staticmethod @staticmethod
def get_namespaces(): def get_namespaces():
@ -654,8 +692,9 @@ class ActionsMap(object):
subcategories = {} subcategories = {}
# Get category parser # Get category parser
category_parser = top_parser.add_category_parser(category_name, category_parser = top_parser.add_category_parser(
**category_values) category_name, **category_values
)
# action_name is like "list" of "domain list" # action_name is like "list" of "domain list"
# action_options are the values # action_options are the values
@ -664,19 +703,21 @@ class ActionsMap(object):
tid = (namespace, category_name, action_name) tid = (namespace, category_name, action_name)
# Get action parser # Get action parser
action_parser = category_parser.add_action_parser(action_name, action_parser = category_parser.add_action_parser(
tid, action_name, tid, **action_options
**action_options) )
if action_parser is None: # No parser for the action if action_parser is None: # No parser for the action
continue continue
# Store action identifier and add arguments # Store action identifier and add arguments
action_parser.set_defaults(_tid=tid) action_parser.set_defaults(_tid=tid)
action_parser.add_arguments(arguments, action_parser.add_arguments(
arguments,
extraparser=self.extraparser, extraparser=self.extraparser,
format_arg_names=top_parser.format_arg_names, format_arg_names=top_parser.format_arg_names,
validate_extra=validate_extra) validate_extra=validate_extra,
)
if 'configuration' in action_options: if 'configuration' in action_options:
category_parser.set_conf(tid, action_options['configuration']) category_parser.set_conf(tid, action_options['configuration'])
@ -688,7 +729,9 @@ class ActionsMap(object):
actions = subcategory_values.pop('actions') actions = subcategory_values.pop('actions')
# Get subcategory parser # Get subcategory parser
subcategory_parser = category_parser.add_subcategory_parser(subcategory_name, **subcategory_values) subcategory_parser = category_parser.add_subcategory_parser(
subcategory_name, **subcategory_values
)
# action_name is like "status" of "domain cert status" # action_name is like "status" of "domain cert status"
# action_options are the values # action_options are the values
@ -698,19 +741,25 @@ class ActionsMap(object):
try: try:
# Get action parser # Get action parser
action_parser = subcategory_parser.add_action_parser(action_name, tid, **action_options) action_parser = subcategory_parser.add_action_parser(
action_name, tid, **action_options
)
except AttributeError: except AttributeError:
# No parser for the action # No parser for the action
continue continue
# Store action identifier and add arguments # Store action identifier and add arguments
action_parser.set_defaults(_tid=tid) action_parser.set_defaults(_tid=tid)
action_parser.add_arguments(arguments, action_parser.add_arguments(
arguments,
extraparser=self.extraparser, extraparser=self.extraparser,
format_arg_names=top_parser.format_arg_names, format_arg_names=top_parser.format_arg_names,
validate_extra=validate_extra) validate_extra=validate_extra,
)
if 'configuration' in action_options: if 'configuration' in action_options:
category_parser.set_conf(tid, action_options['configuration']) category_parser.set_conf(
tid, action_options['configuration']
)
return top_parser return top_parser

View file

@ -11,6 +11,7 @@ logger = logging.getLogger('moulinette.authenticator')
# Base Class ----------------------------------------------------------- # Base Class -----------------------------------------------------------
class BaseAuthenticator(object): class BaseAuthenticator(object):
"""Authenticator base representation """Authenticator base representation
@ -46,8 +47,9 @@ class BaseAuthenticator(object):
@property @property
def is_authenticated(self): def is_authenticated(self):
"""Either the instance is authenticated or not""" """Either the instance is authenticated or not"""
raise NotImplementedError("derived class '%s' must override this property" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this property" % self.__class__.__name__
)
# Virtual methods # Virtual methods
# Each authenticator classes must implement these methods. # Each authenticator classes must implement these methods.
@ -62,8 +64,9 @@ class BaseAuthenticator(object):
- password -- A clear text password - password -- A clear text password
""" """
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
# Authentication methods # Authentication methods
@ -94,7 +97,9 @@ class BaseAuthenticator(object):
# Extract id and hash from token # Extract id and hash from token
s_id, s_hash = token s_id, s_hash = token
except TypeError as e: except TypeError as e:
logger.error("unable to extract token parts from '%s' because '%s'", token, e) logger.error(
"unable to extract token parts from '%s' because '%s'", token, e
)
if password is None: if password is None:
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
@ -111,8 +116,12 @@ class BaseAuthenticator(object):
except MoulinetteError: except MoulinetteError:
raise raise
except Exception as e: except Exception as e:
logger.exception("authentication (name: '%s', vendor: '%s') fails because '%s'", logger.exception(
self.name, self.vendor, e) "authentication (name: '%s', vendor: '%s') fails because '%s'",
self.name,
self.vendor,
e,
)
raise MoulinetteError('unable_authenticate') raise MoulinetteError('unable_authenticate')
# Store session # Store session
@ -121,6 +130,7 @@ class BaseAuthenticator(object):
self._store_session(s_id, s_hash, password) self._store_session(s_id, s_hash, password)
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
logger.exception("unable to store session because %s", e) logger.exception("unable to store session because %s", e)
else: else:
@ -132,8 +142,9 @@ class BaseAuthenticator(object):
def _open_sessionfile(self, session_id, mode='r'): def _open_sessionfile(self, session_id, mode='r'):
"""Open a session file for this instance in given mode""" """Open a session file for this instance in given mode"""
return open_cachefile('%s.asc' % session_id, mode, return open_cachefile(
subdir='session/%s' % self.name) '%s.asc' % session_id, mode, subdir='session/%s' % self.name
)
def _store_session(self, session_id, session_hash, password): def _store_session(self, session_id, session_hash, password):
"""Store a session and its associated password""" """Store a session and its associated password"""
@ -142,7 +153,9 @@ class BaseAuthenticator(object):
# Encrypt the password using the session hash # Encrypt the password using the session hash
s = str(gpg.encrypt(password, None, symmetric=True, passphrase=session_hash)) s = str(gpg.encrypt(password, None, symmetric=True, passphrase=session_hash))
assert len(s), "For some reason GPG can't perform encryption, maybe check /root/.gnupg/gpg.conf or re-run with gpg = gnupg.GPG(verbose=True) ?" assert len(
s
), "For some reason GPG can't perform encryption, maybe check /root/.gnupg/gpg.conf or re-run with gpg = gnupg.GPG(verbose=True) ?"
with self._open_sessionfile(session_id, 'w') as f: with self._open_sessionfile(session_id, 'w') as f:
f.write(s) f.write(s)
@ -161,7 +174,11 @@ class BaseAuthenticator(object):
decrypted = gpg.decrypt(enc_pwd, passphrase=session_hash) decrypted = gpg.decrypt(enc_pwd, passphrase=session_hash)
if decrypted.ok is not True: if decrypted.ok is not True:
error_message = "unable to decrypt password for the session: %s" % decrypted.status error_message = (
"unable to decrypt password for the session: %s" % decrypted.status
)
logger.error(error_message) logger.error(error_message)
raise MoulinetteError('unable_retrieve_session', exception=error_message) raise MoulinetteError(
'unable_retrieve_session', exception=error_message
)
return decrypted.data return decrypted.data

View file

@ -18,6 +18,7 @@ logger = logging.getLogger('moulinette.authenticator.ldap')
# LDAP Class Implementation -------------------------------------------- # LDAP Class Implementation --------------------------------------------
class Authenticator(BaseAuthenticator): class Authenticator(BaseAuthenticator):
"""LDAP Authenticator """LDAP Authenticator
@ -34,8 +35,14 @@ class Authenticator(BaseAuthenticator):
""" """
def __init__(self, name, uri, base_dn, user_rdn=None): def __init__(self, name, uri, base_dn, user_rdn=None):
logger.debug("initialize authenticator '%s' with: uri='%s', " logger.debug(
"base_dn='%s', user_rdn='%s'", name, uri, base_dn, user_rdn) "initialize authenticator '%s' with: uri='%s', "
"base_dn='%s', user_rdn='%s'",
name,
uri,
base_dn,
user_rdn,
)
super(Authenticator, self).__init__(name) super(Authenticator, self).__init__(name)
self.uri = uri self.uri = uri
@ -79,7 +86,9 @@ class Authenticator(BaseAuthenticator):
def authenticate(self, password): def authenticate(self, password):
try: try:
con = ldap.ldapobject.ReconnectLDAPObject(self.uri, retry_max=10, retry_delay=0.5) con = ldap.ldapobject.ReconnectLDAPObject(
self.uri, retry_max=10, retry_delay=0.5
)
if self.userdn: if self.userdn:
if 'cn=external,cn=auth' in self.userdn: if 'cn=external,cn=auth' in self.userdn:
con.sasl_non_interactive_bind_s('EXTERNAL') con.sasl_non_interactive_bind_s('EXTERNAL')
@ -99,13 +108,16 @@ class Authenticator(BaseAuthenticator):
def _ensure_password_uses_strong_hash(self, password): def _ensure_password_uses_strong_hash(self, password):
# XXX this has been copy pasted from YunoHost, should we put that into moulinette? # XXX this has been copy pasted from YunoHost, should we put that into moulinette?
def _hash_user_password(password): def _hash_user_password(password):
char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" char_set = (
string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
)
salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)]) salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)])
salt = '$6$' + salt + '$' salt = '$6$' + salt + '$'
return '{CRYPT}' + crypt.crypt(str(password), salt) return '{CRYPT}' + crypt.crypt(str(password), salt)
hashed_password = self.search("cn=admin,dc=yunohost,dc=org", hashed_password = self.search(
attrs=["userPassword"])[0] "cn=admin,dc=yunohost,dc=org", attrs=["userPassword"]
)[0]
# post-install situation, password is not already set # post-install situation, password is not already set
if "userPassword" not in hashed_password or not hashed_password["userPassword"]: if "userPassword" not in hashed_password or not hashed_password["userPassword"]:
@ -113,9 +125,7 @@ class Authenticator(BaseAuthenticator):
# we aren't using sha-512 but something else that is weaker, proceed to upgrade # we aren't using sha-512 but something else that is weaker, proceed to upgrade
if not hashed_password["userPassword"][0].startswith("{CRYPT}$6$"): if not hashed_password["userPassword"][0].startswith("{CRYPT}$6$"):
self.update("cn=admin", { self.update("cn=admin", {"userPassword": _hash_user_password(password)})
"userPassword": _hash_user_password(password),
})
# Additional LDAP methods # Additional LDAP methods
# TODO: Review these methods # TODO: Review these methods
@ -141,8 +151,14 @@ class Authenticator(BaseAuthenticator):
try: try:
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs) result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
except Exception as e: except Exception as e:
logger.exception("error during LDAP search operation with: base='%s', " logger.exception(
"filter='%s', attrs=%s and exception %s", base, filter, attrs, e) "error during LDAP search operation with: base='%s', "
"filter='%s', attrs=%s and exception %s",
base,
filter,
attrs,
e,
)
raise MoulinetteError('ldap_operation_error') raise MoulinetteError('ldap_operation_error')
result_list = [] result_list = []
@ -172,8 +188,13 @@ class Authenticator(BaseAuthenticator):
try: try:
self.con.add_s(dn, ldif) self.con.add_s(dn, ldif)
except Exception as e: except Exception as e:
logger.exception("error during LDAP add operation with: rdn='%s', " logger.exception(
"attr_dict=%s and exception %s", rdn, attr_dict, e) "error during LDAP add operation with: rdn='%s', "
"attr_dict=%s and exception %s",
rdn,
attr_dict,
e,
)
raise MoulinetteError('ldap_operation_error') raise MoulinetteError('ldap_operation_error')
else: else:
return True return True
@ -193,7 +214,11 @@ class Authenticator(BaseAuthenticator):
try: try:
self.con.delete_s(dn) self.con.delete_s(dn)
except Exception as e: except Exception as e:
logger.exception("error during LDAP delete operation with: rdn='%s' and exception %s", rdn, e) logger.exception(
"error during LDAP delete operation with: rdn='%s' and exception %s",
rdn,
e,
)
raise MoulinetteError('ldap_operation_error') raise MoulinetteError('ldap_operation_error')
else: else:
return True return True
@ -222,9 +247,14 @@ class Authenticator(BaseAuthenticator):
self.con.modify_ext_s(dn, ldif) self.con.modify_ext_s(dn, ldif)
except Exception as e: except Exception as e:
logger.exception("error during LDAP update operation with: rdn='%s', " logger.exception(
"attr_dict=%s, new_rdn=%s and exception: %s", rdn, attr_dict, "error during LDAP update operation with: rdn='%s', "
new_rdn, e) "attr_dict=%s, new_rdn=%s and exception: %s",
rdn,
attr_dict,
new_rdn,
e,
)
raise MoulinetteError('ldap_operation_error') raise MoulinetteError('ldap_operation_error')
else: else:
return True return True
@ -242,11 +272,16 @@ class Authenticator(BaseAuthenticator):
""" """
attr_found = self.get_conflict(value_dict) attr_found = self.get_conflict(value_dict)
if attr_found: if attr_found:
logger.info("attribute '%s' with value '%s' is not unique", logger.info(
attr_found[0], attr_found[1]) "attribute '%s' with value '%s' is not unique",
raise MoulinetteError('ldap_attribute_already_exists', attr_found[0],
attr_found[1],
)
raise MoulinetteError(
'ldap_attribute_already_exists',
attribute=attr_found[0], attribute=attr_found[0],
value=attr_found[1]) value=attr_found[1],
)
return True return True
def get_conflict(self, value_dict, base_dn=None): def get_conflict(self, value_dict, base_dn=None):

View file

@ -40,8 +40,7 @@ def open_cachefile(filename, mode='r', **kwargs):
""" """
# Set make_dir if not given # Set make_dir if not given
kwargs['make_dir'] = kwargs.get('make_dir', kwargs['make_dir'] = kwargs.get('make_dir', True if mode[0] == 'w' else False)
True if mode[0] == 'w' else False)
cache_dir = get_cachedir(**kwargs) cache_dir = get_cachedir(**kwargs)
file_path = os.path.join(cache_dir, filename) file_path = os.path.join(cache_dir, filename)
return open(file_path, mode) return open(file_path, mode)

View file

@ -21,6 +21,7 @@ def during_unittests_run():
# Internationalization ------------------------------------------------- # Internationalization -------------------------------------------------
class Translator(object): class Translator(object):
"""Internationalization class """Internationalization class
@ -41,8 +42,9 @@ class Translator(object):
# Attempt to load default translations # Attempt to load default translations
if not self._load_translations(default_locale): if not self._load_translations(default_locale):
logger.error("unable to load locale '%s' from '%s'", logger.error(
default_locale, locale_dir) "unable to load locale '%s' from '%s'", default_locale, locale_dir
)
self.default_locale = default_locale self.default_locale = default_locale
def get_locales(self): def get_locales(self):
@ -70,8 +72,11 @@ class Translator(object):
""" """
if locale not in self._translations: if locale not in self._translations:
if not self._load_translations(locale): if not self._load_translations(locale):
logger.debug("unable to load locale '%s' from '%s'", logger.debug(
self.default_locale, self.locale_dir) "unable to load locale '%s' from '%s'",
self.default_locale,
self.locale_dir,
)
# Revert to default locale # Revert to default locale
self.locale = self.default_locale self.locale = self.default_locale
@ -94,11 +99,18 @@ class Translator(object):
failed_to_format = False failed_to_format = False
if key in self._translations.get(self.locale, {}): if key in self._translations.get(self.locale, {}):
try: try:
return self._translations[self.locale][key].encode('utf-8').format(*args, **kwargs) return (
self._translations[self.locale][key]
.encode('utf-8')
.format(*args, **kwargs)
)
except KeyError as e: except KeyError as e:
unformatted_string = self._translations[self.locale][key].encode('utf-8') unformatted_string = self._translations[self.locale][key].encode(
error_message = "Failed to format translated string '%s': '%s' with arguments '%s' and '%s, raising error: %s(%s) (don't panic this is just a warning)" % ( 'utf-8'
key, unformatted_string, args, kwargs, e.__class__.__name__, e )
error_message = (
"Failed to format translated string '%s': '%s' with arguments '%s' and '%s, raising error: %s(%s) (don't panic this is just a warning)"
% (key, unformatted_string, args, kwargs, e.__class__.__name__, e)
) )
if not during_unittests_run(): if not during_unittests_run():
@ -108,16 +120,25 @@ class Translator(object):
failed_to_format = True failed_to_format = True
if failed_to_format or (self.default_locale != self.locale and key in self._translations.get(self.default_locale, {})): if failed_to_format or (
logger.info("untranslated key '%s' for locale '%s'", self.default_locale != self.locale
key, self.locale) and key in self._translations.get(self.default_locale, {})
):
logger.info("untranslated key '%s' for locale '%s'", key, self.locale)
try: try:
return self._translations[self.default_locale][key].encode('utf-8').format(*args, **kwargs) return (
self._translations[self.default_locale][key]
.encode('utf-8')
.format(*args, **kwargs)
)
except KeyError as e: except KeyError as e:
unformatted_string = self._translations[self.default_locale][key].encode('utf-8') unformatted_string = self._translations[self.default_locale][
error_message = "Failed to format translatable string '%s': '%s' with arguments '%s' and '%s', raising error: %s(%s) (don't panic this is just a warning)" % ( key
key, unformatted_string, args, kwargs, e.__class__.__name__, e ].encode('utf-8')
error_message = (
"Failed to format translatable string '%s': '%s' with arguments '%s' and '%s', raising error: %s(%s) (don't panic this is just a warning)"
% (key, unformatted_string, args, kwargs, e.__class__.__name__, e)
) )
if not during_unittests_run(): if not during_unittests_run():
logger.exception(error_message) logger.exception(error_message)
@ -126,7 +147,10 @@ class Translator(object):
return self._translations[self.default_locale][key].encode('utf-8') return self._translations[self.default_locale][key].encode('utf-8')
error_message = "unable to retrieve string to translate with key '%s' for default locale 'locales/%s.json' file (don't panic this is just a warning)" % (key, self.default_locale) error_message = (
"unable to retrieve string to translate with key '%s' for default locale 'locales/%s.json' file (don't panic this is just a warning)"
% (key, self.default_locale)
)
if not during_unittests_run(): if not during_unittests_run():
logger.exception(error_message) logger.exception(error_message)
@ -202,8 +226,9 @@ class Moulinette18n(object):
""" """
if namespace not in self._namespaces: if namespace not in self._namespaces:
# Create new Translator object # Create new Translator object
translator = Translator('%s/%s/locales' % (self.lib_dir, namespace), translator = Translator(
self.default_locale) '%s/%s/locales' % (self.lib_dir, namespace), self.default_locale
)
translator.set_locale(self.locale) translator.set_locale(self.locale)
self._namespaces[namespace] = translator self._namespaces[namespace] = translator
@ -354,6 +379,7 @@ class MoulinetteSignals(object):
# Interfaces & Authenticators management ------------------------------- # Interfaces & Authenticators management -------------------------------
def init_interface(name, kwargs={}, actionsmap={}): def init_interface(name, kwargs={}, actionsmap={}):
"""Return a new interface instance """Return a new interface instance
@ -444,6 +470,7 @@ def clean_session(session_id, profiles=[]):
# Moulinette core classes ---------------------------------------------- # Moulinette core classes ----------------------------------------------
class MoulinetteError(Exception): class MoulinetteError(Exception):
"""Moulinette base exception""" """Moulinette base exception"""
@ -473,7 +500,7 @@ class MoulinetteLock(object):
""" """
def __init__(self, namespace, timeout=None, interval=.5): def __init__(self, namespace, timeout=None, interval=0.5):
self.namespace = namespace self.namespace = namespace
self.timeout = timeout self.timeout = timeout
self.interval = interval self.interval = interval
@ -527,9 +554,13 @@ class MoulinetteLock(object):
# warn the user if it's been too much time since they are waiting # warn the user if it's been too much time since they are waiting
if (time.time() - start_time) > warning_treshold: if (time.time() - start_time) > warning_treshold:
if warning_treshold == 15: if warning_treshold == 15:
logger.warning(moulinette.m18n.g('warn_the_user_about_waiting_lock')) logger.warning(
moulinette.m18n.g('warn_the_user_about_waiting_lock')
)
else: else:
logger.warning(moulinette.m18n.g('warn_the_user_about_waiting_lock_again')) logger.warning(
moulinette.m18n.g('warn_the_user_about_waiting_lock_again')
)
warning_treshold *= 4 warning_treshold *= 4
# Wait before checking again # Wait before checking again
@ -552,7 +583,9 @@ class MoulinetteLock(object):
if os.path.exists(self._lockfile): if os.path.exists(self._lockfile):
os.unlink(self._lockfile) os.unlink(self._lockfile)
else: else:
logger.warning("Uhoh, somehow the lock %s did not exist ..." % self._lockfile) logger.warning(
"Uhoh, somehow the lock %s did not exist ..." % self._lockfile
)
logger.debug('lock has been released') logger.debug('lock has been released')
self._locked = False self._locked = False

View file

@ -7,6 +7,8 @@ def init_moulinette_env():
return { return {
'DATA_DIR': environ.get('MOULINETTE_DATA_DIR', '/usr/share/moulinette'), 'DATA_DIR': environ.get('MOULINETTE_DATA_DIR', '/usr/share/moulinette'),
'LIB_DIR': environ.get('MOULINETTE_LIB_DIR', '/usr/lib/moulinette'), 'LIB_DIR': environ.get('MOULINETTE_LIB_DIR', '/usr/lib/moulinette'),
'LOCALES_DIR': environ.get('MOULINETTE_LOCALES_DIR', '/usr/share/moulinette/locale'), 'LOCALES_DIR': environ.get(
'MOULINETTE_LOCALES_DIR', '/usr/share/moulinette/locale'
),
'CACHE_DIR': environ.get('MOULINETTE_CACHE_DIR', '/var/cache/moulinette'), 'CACHE_DIR': environ.get('MOULINETTE_CACHE_DIR', '/var/cache/moulinette'),
} }

View file

@ -7,7 +7,7 @@ import copy
from collections import deque, OrderedDict from collections import deque, OrderedDict
from moulinette import msignals, msettings, m18n from moulinette import msignals, msettings, m18n
from moulinette.core import (init_authenticator, MoulinetteError) from moulinette.core import init_authenticator, MoulinetteError
logger = logging.getLogger('moulinette.interface') logger = logging.getLogger('moulinette.interface')
@ -18,6 +18,7 @@ CALLBACKS_PROP = '_callbacks'
# Base Class ----------------------------------------------------------- # Base Class -----------------------------------------------------------
class BaseActionsMapParser(object): class BaseActionsMapParser(object):
"""Actions map's base Parser """Actions map's base Parser
@ -37,8 +38,7 @@ class BaseActionsMapParser(object):
if parent: if parent:
self._o = parent self._o = parent
else: else:
logger.debug('initializing base actions map parser for %s', logger.debug('initializing base actions map parser for %s', self.interface)
self.interface)
msettings['interface'] = self.interface msettings['interface'] = self.interface
self._o = self self._o = self
@ -70,8 +70,9 @@ class BaseActionsMapParser(object):
A list of option strings A list of option strings
""" """
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
def has_global_parser(self): def has_global_parser(self):
return False return False
@ -85,8 +86,9 @@ class BaseActionsMapParser(object):
An ArgumentParser based object An ArgumentParser based object
""" """
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
def add_category_parser(self, name, **kwargs): def add_category_parser(self, name, **kwargs):
"""Add a parser for a category """Add a parser for a category
@ -100,8 +102,9 @@ class BaseActionsMapParser(object):
A BaseParser based object A BaseParser based object
""" """
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
def add_action_parser(self, name, tid, **kwargs): def add_action_parser(self, name, tid, **kwargs):
"""Add a parser for an action """Add a parser for an action
@ -116,8 +119,9 @@ class BaseActionsMapParser(object):
An ArgumentParser based object An ArgumentParser based object
""" """
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
def parse_args(self, args, **kwargs): def parse_args(self, args, **kwargs):
"""Parse arguments """Parse arguments
@ -132,16 +136,18 @@ class BaseActionsMapParser(object):
The populated namespace The populated namespace
""" """
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
# Arguments helpers # Arguments helpers
def prepare_action_namespace(self, tid, namespace=None): def prepare_action_namespace(self, tid, namespace=None):
"""Prepare the namespace for a given action""" """Prepare the namespace for a given action"""
# Validate tid and namespace # Validate tid and namespace
if not isinstance(tid, tuple) and \ if not isinstance(tid, tuple) and (
(namespace is None or not hasattr(namespace, TO_RETURN_PROP)): namespace is None or not hasattr(namespace, TO_RETURN_PROP)
):
raise MoulinetteError('invalid_usage') raise MoulinetteError('invalid_usage')
elif not tid: elif not tid:
tid = GLOBAL_SECTION tid = GLOBAL_SECTION
@ -159,8 +165,10 @@ class BaseActionsMapParser(object):
auth = msignals.authenticate(cls(), **auth_conf) auth = msignals.authenticate(cls(), **auth_conf)
if not auth.is_authenticated: if not auth.is_authenticated:
raise MoulinetteError('authentication_required_long') raise MoulinetteError('authentication_required_long')
if self.get_conf(tid, 'argument_auth') and \ if (
self.get_conf(tid, 'authenticate') == 'all': self.get_conf(tid, 'argument_auth')
and self.get_conf(tid, 'authenticate') == 'all'
):
namespace.auth = auth namespace.auth = auth
return namespace return namespace
@ -260,8 +268,11 @@ class BaseActionsMapParser(object):
# Store only if authentication is needed # Store only if authentication is needed
conf['authenticate'] = True if self.interface in ifaces else False conf['authenticate'] = True if self.interface in ifaces else False
else: else:
logger.error("expecting 'all', 'False' or a list for " logger.error(
"configuration 'authenticate', got %r", ifaces) "expecting 'all', 'False' or a list for "
"configuration 'authenticate', got %r",
ifaces,
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
# -- 'authenticator' # -- 'authenticator'
@ -275,13 +286,18 @@ class BaseActionsMapParser(object):
# Store needed authenticator profile # Store needed authenticator profile
conf['authenticator'] = self.global_conf['authenticator'][auth] conf['authenticator'] = self.global_conf['authenticator'][auth]
except KeyError: except KeyError:
logger.error("requesting profile '%s' which is undefined in " logger.error(
"global configuration of 'authenticator'", auth) "requesting profile '%s' which is undefined in "
"global configuration of 'authenticator'",
auth,
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
elif is_global and isinstance(auth, dict): elif is_global and isinstance(auth, dict):
if len(auth) == 0: if len(auth) == 0:
logger.warning('no profile defined in global configuration ' logger.warning(
"for 'authenticator'") 'no profile defined in global configuration '
"for 'authenticator'"
)
else: else:
auths = {} auths = {}
for auth_name, auth_conf in auth.items(): for auth_name, auth_conf in auth.items():
@ -293,13 +309,18 @@ class BaseActionsMapParser(object):
# configuration (i.e. 'help') # configuration (i.e. 'help')
# - parameters: a dict of arguments for the # - parameters: a dict of arguments for the
# authenticator profile # authenticator profile
auths[auth_name] = ((auth_conf.get('vendor'), auth_name), auths[auth_name] = (
(auth_conf.get('vendor'), auth_name),
{'help': auth_conf.get('help', None)}, {'help': auth_conf.get('help', None)},
auth_conf.get('parameters', {})) auth_conf.get('parameters', {}),
)
conf['authenticator'] = auths conf['authenticator'] = auths
else: else:
logger.error("expecting a dict of profile(s) or a profile name " logger.error(
"for configuration 'authenticator', got %r", auth) "expecting a dict of profile(s) or a profile name "
"for configuration 'authenticator', got %r",
auth,
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
# -- 'argument_auth' # -- 'argument_auth'
@ -311,8 +332,10 @@ class BaseActionsMapParser(object):
if isinstance(arg_auth, bool): if isinstance(arg_auth, bool):
conf['argument_auth'] = arg_auth conf['argument_auth'] = arg_auth
else: else:
logger.error("expecting a boolean for configuration " logger.error(
"'argument_auth', got %r", arg_auth) "expecting a boolean for configuration " "'argument_auth', got %r",
arg_auth,
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
# -- 'lock' # -- 'lock'
@ -324,8 +347,9 @@ class BaseActionsMapParser(object):
if isinstance(lock, bool): if isinstance(lock, bool):
conf['lock'] = lock conf['lock'] = lock
else: else:
logger.error("expecting a boolean for configuration 'lock', " logger.error(
"got %r", lock) "expecting a boolean for configuration 'lock', " "got %r", lock
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
return conf return conf
@ -346,8 +370,7 @@ class BaseActionsMapParser(object):
# Return global configuration and an authenticator # Return global configuration and an authenticator
# instanciator as a 2-tuple # instanciator as a 2-tuple
return (configuration, return (configuration, lambda: init_authenticator(identifier, parameters))
lambda: init_authenticator(identifier, parameters))
return value return value
@ -364,38 +387,45 @@ class BaseInterface(object):
- actionsmap -- The ActionsMap instance to connect to - actionsmap -- The ActionsMap instance to connect to
""" """
# TODO: Add common interface methods and try to standardize default ones # TODO: Add common interface methods and try to standardize default ones
def __init__(self, actionsmap): def __init__(self, actionsmap):
raise NotImplementedError("derived class '%s' must override this method" % raise NotImplementedError(
self.__class__.__name__) "derived class '%s' must override this method" % self.__class__.__name__
)
# Argument parser ------------------------------------------------------ # Argument parser ------------------------------------------------------
class _CallbackAction(argparse.Action):
def __init__(self, class _CallbackAction(argparse.Action):
def __init__(
self,
option_strings, option_strings,
dest, dest,
nargs=0, nargs=0,
callback={}, callback={},
default=argparse.SUPPRESS, default=argparse.SUPPRESS,
help=None): help=None,
):
if not callback or 'method' not in callback: if not callback or 'method' not in callback:
raise ValueError('callback must be provided with at least ' raise ValueError('callback must be provided with at least ' 'a method key')
'a method key')
super(_CallbackAction, self).__init__( super(_CallbackAction, self).__init__(
option_strings=option_strings, option_strings=option_strings,
dest=dest, dest=dest,
nargs=nargs, nargs=nargs,
default=default, default=default,
help=help) help=help,
)
self.callback_method = callback.get('method') self.callback_method = callback.get('method')
self.callback_kwargs = callback.get('kwargs', {}) self.callback_kwargs = callback.get('kwargs', {})
self.callback_return = callback.get('return', False) self.callback_return = callback.get('return', False)
logger.debug("registering new callback action '{0}' to {1}".format( logger.debug(
self.callback_method, option_strings)) "registering new callback action '{0}' to {1}".format(
self.callback_method, option_strings
)
)
@property @property
def callback(self): def callback(self):
@ -407,12 +437,10 @@ class _CallbackAction(argparse.Action):
# Attempt to retrieve callback method # Attempt to retrieve callback method
mod_name, func_name = (self.callback_method).rsplit('.', 1) mod_name, func_name = (self.callback_method).rsplit('.', 1)
try: try:
mod = __import__(mod_name, globals=globals(), level=0, mod = __import__(mod_name, globals=globals(), level=0, fromlist=[func_name])
fromlist=[func_name])
func = getattr(mod, func_name) func = getattr(mod, func_name)
except (AttributeError, ImportError): except (AttributeError, ImportError):
raise ValueError('unable to import method {0}'.format( raise ValueError('unable to import method {0}'.format(self.callback_method))
self.callback_method))
self._callback = func self._callback = func
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
@ -425,8 +453,10 @@ class _CallbackAction(argparse.Action):
# Execute callback and get returned value # Execute callback and get returned value
value = self.callback(namespace, values, **self.callback_kwargs) value = self.callback(namespace, values, **self.callback_kwargs)
except: except:
logger.exception("cannot get value from callback method " logger.exception(
"'{0}'".format(self.callback_method)) "cannot get value from callback method "
"'{0}'".format(self.callback_method)
)
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')
else: else:
if value: if value:
@ -467,8 +497,7 @@ class _ExtendedSubParsersAction(argparse._SubParsersAction):
if 'help' in kwargs: if 'help' in kwargs:
del kwargs['help'] del kwargs['help']
parser = super(_ExtendedSubParsersAction, self).add_parser( parser = super(_ExtendedSubParsersAction, self).add_parser(name, **kwargs)
name, **kwargs)
# Append each deprecated command alias name # Append each deprecated command alias name
for command in deprecated_alias: for command in deprecated_alias:
@ -490,23 +519,30 @@ class _ExtendedSubParsersAction(argparse._SubParsersAction):
else: else:
# Warn the user about deprecated command # Warn the user about deprecated command
if correct_name is None: if correct_name is None:
logger.warning(m18n.g('deprecated_command', prog=parser.prog, logger.warning(
command=parser_name)) m18n.g('deprecated_command', prog=parser.prog, command=parser_name)
)
else: else:
logger.warning(m18n.g('deprecated_command_alias', logger.warning(
old=parser_name, new=correct_name, m18n.g(
prog=parser.prog)) 'deprecated_command_alias',
old=parser_name,
new=correct_name,
prog=parser.prog,
)
)
values[0] = correct_name values[0] = correct_name
return super(_ExtendedSubParsersAction, self).__call__( return super(_ExtendedSubParsersAction, self).__call__(
parser, namespace, values, option_string) parser, namespace, values, option_string
)
class ExtendedArgumentParser(argparse.ArgumentParser): class ExtendedArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ExtendedArgumentParser, self).__init__(formatter_class=PositionalsFirstHelpFormatter, super(ExtendedArgumentParser, self).__init__(
*args, **kwargs) formatter_class=PositionalsFirstHelpFormatter, *args, **kwargs
)
# Register additional actions # Register additional actions
self.register('action', 'callback', _CallbackAction) self.register('action', 'callback', _CallbackAction)
@ -538,11 +574,14 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
queue = list() queue = list()
return queue return queue
def add_arguments(self, arguments, extraparser, format_arg_names=None, validate_extra=True): def add_arguments(
self, arguments, extraparser, format_arg_names=None, validate_extra=True
):
for argument_name, argument_options in arguments.items(): for argument_name, argument_options in arguments.items():
# will adapt arguments name for cli or api context # will adapt arguments name for cli or api context
names = format_arg_names(str(argument_name), names = format_arg_names(
argument_options.pop('full', None)) str(argument_name), argument_options.pop('full', None)
)
if "type" in argument_options: if "type" in argument_options:
argument_options['type'] = eval(argument_options['type']) argument_options['type'] = eval(argument_options['type'])
@ -550,8 +589,9 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
if "extra" in argument_options: if "extra" in argument_options:
extra = argument_options.pop('extra') extra = argument_options.pop('extra')
argument_dest = self.add_argument(*names, **argument_options).dest argument_dest = self.add_argument(*names, **argument_options).dest
extraparser.add_argument(self.get_default("_tid"), extraparser.add_argument(
argument_dest, extra, validate_extra) self.get_default("_tid"), argument_dest, extra, validate_extra
)
continue continue
self.add_argument(*names, **argument_options) self.add_argument(*names, **argument_options)
@ -560,8 +600,7 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
if action.nargs == argparse.PARSER and not action.required: if action.nargs == argparse.PARSER and not action.required:
return '([-AO]*)' return '([-AO]*)'
else: else:
return super(ExtendedArgumentParser, self)._get_nargs_pattern( return super(ExtendedArgumentParser, self)._get_nargs_pattern(action)
action)
def _get_values(self, action, arg_strings): def _get_values(self, action, arg_strings):
if action.nargs == argparse.PARSER and not action.required: if action.nargs == argparse.PARSER and not action.required:
@ -571,8 +610,7 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
else: else:
value = argparse.SUPPRESS value = argparse.SUPPRESS
else: else:
value = super(ExtendedArgumentParser, self)._get_values( value = super(ExtendedArgumentParser, self)._get_values(action, arg_strings)
action, arg_strings)
return value return value
# Adapted from : # Adapted from :
@ -581,8 +619,7 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
formatter = self._get_formatter() formatter = self._get_formatter()
# usage # usage
formatter.add_usage(self.usage, self._actions, formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
self._mutually_exclusive_groups)
# description # description
formatter.add_text(self.description) formatter.add_text(self.description)
@ -600,14 +637,30 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
subcategories_subparser = copy.copy(action_group._group_actions[0]) subcategories_subparser = copy.copy(action_group._group_actions[0])
# Filter "action"-type and "subcategory"-type commands # Filter "action"-type and "subcategory"-type commands
actions_subparser.choices = OrderedDict([(k, v) for k, v in actions_subparser.choices.items() if v.type == "action"]) actions_subparser.choices = OrderedDict(
subcategories_subparser.choices = OrderedDict([(k, v) for k, v in subcategories_subparser.choices.items() if v.type == "subcategory"]) [
(k, v)
for k, v in actions_subparser.choices.items()
if v.type == "action"
]
)
subcategories_subparser.choices = OrderedDict(
[
(k, v)
for k, v in subcategories_subparser.choices.items()
if v.type == "subcategory"
]
)
actions_choices = actions_subparser.choices.keys() actions_choices = actions_subparser.choices.keys()
subcategories_choices = subcategories_subparser.choices.keys() subcategories_choices = subcategories_subparser.choices.keys()
actions_subparser._choices_actions = [c for c in choice_actions if c.dest in actions_choices] actions_subparser._choices_actions = [
subcategories_subparser._choices_actions = [c for c in choice_actions if c.dest in subcategories_choices] c for c in choice_actions if c.dest in actions_choices
]
subcategories_subparser._choices_actions = [
c for c in choice_actions if c.dest in subcategories_choices
]
# Display each section (actions and subcategories) # Display each section (actions and subcategories)
if actions_choices != []: if actions_choices != []:
@ -642,7 +695,6 @@ class ExtendedArgumentParser(argparse.ArgumentParser):
# and fix is inspired from here : # and fix is inspired from here :
# https://stackoverflow.com/questions/26985650/argparse-do-not-catch-positional-arguments-with-nargs/26986546#26986546 # https://stackoverflow.com/questions/26985650/argparse-do-not-catch-positional-arguments-with-nargs/26986546#26986546
class PositionalsFirstHelpFormatter(argparse.HelpFormatter): class PositionalsFirstHelpFormatter(argparse.HelpFormatter):
def _format_usage(self, usage, actions, groups, prefix): def _format_usage(self, usage, actions, groups, prefix):
if prefix is None: if prefix is None:
# TWEAK : not using gettext here... # TWEAK : not using gettext here...
@ -706,7 +758,7 @@ class PositionalsFirstHelpFormatter(argparse.HelpFormatter):
if line: if line:
lines.append(indent + ' '.join(line)) lines.append(indent + ' '.join(line))
if prefix is not None: if prefix is not None:
lines[0] = lines[0][len(indent):] lines[0] = lines[0][len(indent) :]
return lines return lines
# if prog is short, follow it with optionals or positionals # if prog is short, follow it with optionals or positionals

View file

@ -16,7 +16,9 @@ from bottle import abort
from moulinette import msignals, m18n, env from moulinette import msignals, m18n, env
from moulinette.core import MoulinetteError, clean_session from moulinette.core import MoulinetteError, clean_session
from moulinette.interfaces import ( from moulinette.interfaces import (
BaseActionsMapParser, BaseInterface, ExtendedArgumentParser, BaseActionsMapParser,
BaseInterface,
ExtendedArgumentParser,
) )
from moulinette.utils import log from moulinette.utils import log
from moulinette.utils.serialize import JSONExtendedEncoder from moulinette.utils.serialize import JSONExtendedEncoder
@ -27,9 +29,9 @@ logger = log.getLogger('moulinette.interface.api')
# API helpers ---------------------------------------------------------- # API helpers ----------------------------------------------------------
CSRF_TYPES = set(["text/plain", CSRF_TYPES = set(
"application/x-www-form-urlencoded", ["text/plain", "application/x-www-form-urlencoded", "multipart/form-data"]
"multipart/form-data"]) )
def is_csrf(): def is_csrf():
@ -53,12 +55,14 @@ def filter_csrf(callback):
abort(403, "CSRF protection") abort(403, "CSRF protection")
else: else:
return callback(*args, **kwargs) return callback(*args, **kwargs)
return wrapper return wrapper
class LogQueues(dict): class LogQueues(dict):
"""Map of session id to queue.""" """Map of session id to queue."""
pass pass
@ -99,9 +103,9 @@ class _HTTPArgumentParser(object):
def __init__(self): def __init__(self):
# Initialize the ArgumentParser object # Initialize the ArgumentParser object
self._parser = ExtendedArgumentParser(usage='', self._parser = ExtendedArgumentParser(
prefix_chars='@', usage='', prefix_chars='@', add_help=False
add_help=False) )
self._parser.error = self._error self._parser.error = self._error
self._positional = [] # list(arg_name) self._positional = [] # list(arg_name)
@ -113,11 +117,14 @@ class _HTTPArgumentParser(object):
def get_default(self, dest): def get_default(self, dest):
return self._parser.get_default(dest) return self._parser.get_default(dest)
def add_arguments(self, arguments, extraparser, format_arg_names=None, validate_extra=True): def add_arguments(
self, arguments, extraparser, format_arg_names=None, validate_extra=True
):
for argument_name, argument_options in arguments.items(): for argument_name, argument_options in arguments.items():
# will adapt arguments name for cli or api context # will adapt arguments name for cli or api context
names = format_arg_names(str(argument_name), names = format_arg_names(
argument_options.pop('full', None)) str(argument_name), argument_options.pop('full', None)
)
if "type" in argument_options: if "type" in argument_options:
argument_options['type'] = eval(argument_options['type']) argument_options['type'] = eval(argument_options['type'])
@ -125,8 +132,9 @@ class _HTTPArgumentParser(object):
if "extra" in argument_options: if "extra" in argument_options:
extra = argument_options.pop('extra') extra = argument_options.pop('extra')
argument_dest = self.add_argument(*names, **argument_options).dest argument_dest = self.add_argument(*names, **argument_options).dest
extraparser.add_argument(self.get_default("_tid"), extraparser.add_argument(
argument_dest, extra, validate_extra) self.get_default("_tid"), argument_dest, extra, validate_extra
)
continue continue
self.add_argument(*names, **argument_options) self.add_argument(*names, **argument_options)
@ -166,12 +174,19 @@ class _HTTPArgumentParser(object):
if isinstance(v, str): if isinstance(v, str):
arg_strings.append(v) arg_strings.append(v)
else: else:
logger.warning("unsupported argument value type %r " logger.warning(
"in %s for option string %s", v, value, "unsupported argument value type %r "
option_string) "in %s for option string %s",
v,
value,
option_string,
)
else: else:
logger.warning("unsupported argument type %r for option " logger.warning(
"string %s", value, option_string) "unsupported argument type %r for option " "string %s",
value,
option_string,
)
return arg_strings return arg_strings
@ -208,6 +223,7 @@ class _ActionsMapPlugin(object):
to serve messages coming from the 'display' signal to serve messages coming from the 'display' signal
""" """
name = 'actionsmap' name = 'actionsmap'
api = 2 api = 2
@ -245,6 +261,7 @@ class _ActionsMapPlugin(object):
except KeyError: except KeyError:
pass pass
return callback(**kwargs) return callback(**kwargs)
return wrapper return wrapper
# Logout wrapper # Logout wrapper
@ -256,18 +273,35 @@ class _ActionsMapPlugin(object):
except KeyError: except KeyError:
pass pass
return callback(**kwargs) return callback(**kwargs)
return wrapper return wrapper
# Append authentication routes # Append authentication routes
app.route('/login', name='login', method='POST', app.route(
callback=self.login, skip=['actionsmap'], apply=_login) '/login',
app.route('/logout', name='logout', method='GET', name='login',
callback=self.logout, skip=['actionsmap'], apply=_logout) method='POST',
callback=self.login,
skip=['actionsmap'],
apply=_login,
)
app.route(
'/logout',
name='logout',
method='GET',
callback=self.logout,
skip=['actionsmap'],
apply=_logout,
)
# Append messages route # Append messages route
if self.use_websocket: if self.use_websocket:
app.route('/messages', name='messages', app.route(
callback=self.messages, skip=['actionsmap']) '/messages',
name='messages',
callback=self.messages,
skip=['actionsmap'],
)
# Append routes from the actions map # Append routes from the actions map
for (m, p) in self.actionsmap.parser.routes: for (m, p) in self.actionsmap.parser.routes:
@ -284,6 +318,7 @@ class _ActionsMapPlugin(object):
context -- An instance of Route context -- An instance of Route
""" """
def _format(value): def _format(value):
if isinstance(value, list) and len(value) == 1: if isinstance(value, list) and len(value) == 1:
return value[0] return value[0]
@ -314,6 +349,7 @@ class _ActionsMapPlugin(object):
# Process the action # Process the action
return callback((request.method, context.rule), params) return callback((request.method, context.rule), params)
return wrapper return wrapper
# Routes callbacks # Routes callbacks
@ -337,8 +373,7 @@ class _ActionsMapPlugin(object):
except KeyError: except KeyError:
s_hashes = {} s_hashes = {}
else: else:
s_hashes = request.get_cookie('session.hashes', s_hashes = request.get_cookie('session.hashes', secret=s_secret) or {}
secret=s_secret) or {}
s_hash = random_ascii() s_hash = random_ascii()
try: try:
@ -358,8 +393,9 @@ class _ActionsMapPlugin(object):
self.secrets[s_id] = s_secret = random_ascii() self.secrets[s_id] = s_secret = random_ascii()
response.set_cookie('session.id', s_id, secure=True) response.set_cookie('session.id', s_id, secure=True)
response.set_cookie('session.hashes', s_hashes, secure=True, response.set_cookie(
secret=s_secret) 'session.hashes', s_hashes, secure=True, secret=s_secret
)
return m18n.g('logged_in') return m18n.g('logged_in')
def logout(self, profile=None): def logout(self, profile=None):
@ -443,10 +479,9 @@ class _ActionsMapPlugin(object):
if isinstance(e, HTTPResponse): if isinstance(e, HTTPResponse):
raise e raise e
import traceback import traceback
tb = traceback.format_exc() tb = traceback.format_exc()
logs = {"route": _route, logs = {"route": _route, "arguments": arguments, "traceback": tb}
"arguments": arguments,
"traceback": tb}
return HTTPErrorResponse(json_encode(logs)) return HTTPErrorResponse(json_encode(logs))
else: else:
return format_for_response(ret) return format_for_response(ret)
@ -470,14 +505,16 @@ class _ActionsMapPlugin(object):
s_id = request.get_cookie('session.id') s_id = request.get_cookie('session.id')
try: try:
s_secret = self.secrets[s_id] s_secret = self.secrets[s_id]
s_hash = request.get_cookie('session.hashes', s_hash = request.get_cookie('session.hashes', secret=s_secret, default={})[
secret=s_secret, default={})[authenticator.name] authenticator.name
]
except KeyError: except KeyError:
if authenticator.name == 'default': if authenticator.name == 'default':
msg = m18n.g('authentication_required') msg = m18n.g('authentication_required')
else: else:
msg = m18n.g('authentication_profile_required', msg = m18n.g(
profile=authenticator.name) 'authentication_profile_required', profile=authenticator.name
)
raise HTTPUnauthorizedResponse(msg) raise HTTPUnauthorizedResponse(msg)
else: else:
return authenticator(token=(s_id, s_hash)) return authenticator(token=(s_id, s_hash))
@ -504,26 +541,23 @@ class _ActionsMapPlugin(object):
# HTTP Responses ------------------------------------------------------- # HTTP Responses -------------------------------------------------------
class HTTPOKResponse(HTTPResponse):
class HTTPOKResponse(HTTPResponse):
def __init__(self, output=''): def __init__(self, output=''):
super(HTTPOKResponse, self).__init__(output, 200) super(HTTPOKResponse, self).__init__(output, 200)
class HTTPBadRequestResponse(HTTPResponse): class HTTPBadRequestResponse(HTTPResponse):
def __init__(self, output=''): def __init__(self, output=''):
super(HTTPBadRequestResponse, self).__init__(output, 400) super(HTTPBadRequestResponse, self).__init__(output, 400)
class HTTPUnauthorizedResponse(HTTPResponse): class HTTPUnauthorizedResponse(HTTPResponse):
def __init__(self, output=''): def __init__(self, output=''):
super(HTTPUnauthorizedResponse, self).__init__(output, 401) super(HTTPUnauthorizedResponse, self).__init__(output, 401)
class HTTPErrorResponse(HTTPResponse): class HTTPErrorResponse(HTTPResponse):
def __init__(self, output=''): def __init__(self, output=''):
super(HTTPErrorResponse, self).__init__(output, 500) super(HTTPErrorResponse, self).__init__(output, 500)
@ -548,6 +582,7 @@ def format_for_response(content):
# API Classes Implementation ------------------------------------------- # API Classes Implementation -------------------------------------------
class ActionsMapParser(BaseActionsMapParser): class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the API """Actions map's Parser for the API
@ -611,8 +646,9 @@ class ActionsMapParser(BaseActionsMapParser):
try: try:
keys.append(self._extract_route(r)) keys.append(self._extract_route(r))
except ValueError as e: except ValueError as e:
logger.warning("cannot add api route '%s' for " logger.warning(
"action %s: %s", r, tid, e) "cannot add api route '%s' for " "action %s: %s", r, tid, e
)
continue continue
if len(keys) == 0: if len(keys) == 0:
raise ValueError("no valid api route found") raise ValueError("no valid api route found")
@ -653,8 +689,10 @@ class ActionsMapParser(BaseActionsMapParser):
auth = msignals.authenticate(klass(), **auth_conf) auth = msignals.authenticate(klass(), **auth_conf)
if not auth.is_authenticated: if not auth.is_authenticated:
raise MoulinetteError('authentication_required_long') raise MoulinetteError('authentication_required_long')
if self.get_conf(tid, 'argument_auth') and \ if (
self.get_conf(tid, 'authenticate') == 'all': self.get_conf(tid, 'argument_auth')
and self.get_conf(tid, 'authenticate') == 'all'
):
ret.auth = auth ret.auth = auth
# TODO: Catch errors? # TODO: Catch errors?
@ -702,8 +740,7 @@ class Interface(BaseInterface):
""" """
def __init__(self, actionsmap, routes={}, use_websocket=True, def __init__(self, actionsmap, routes={}, use_websocket=True, log_queues=None):
log_queues=None):
self.use_websocket = use_websocket self.use_websocket = use_websocket
# Attempt to retrieve log queues from an APIQueueHandler # Attempt to retrieve log queues from an APIQueueHandler
@ -720,6 +757,7 @@ class Interface(BaseInterface):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
response.set_header('Access-Control-Allow-Origin', '*') response.set_header('Access-Control-Allow-Origin', '*')
return callback(*args, **kwargs) return callback(*args, **kwargs)
return wrapper return wrapper
# Attempt to retrieve and set locale # Attempt to retrieve and set locale
@ -738,8 +776,8 @@ class Interface(BaseInterface):
app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues)) app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues))
# Append default routes # Append default routes
# app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET', # app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
# callback=self.doc, skip=['actionsmap']) # callback=self.doc, skip=['actionsmap'])
# Append additional routes # Append additional routes
# TODO: Add optional authentication to those routes? # TODO: Add optional authentication to those routes?
@ -759,22 +797,26 @@ class Interface(BaseInterface):
- port -- Server port to bind to - port -- Server port to bind to
""" """
logger.debug("starting the server instance in %s:%d with websocket=%s", logger.debug(
host, port, self.use_websocket) "starting the server instance in %s:%d with websocket=%s",
host,
port,
self.use_websocket,
)
try: try:
if self.use_websocket: if self.use_websocket:
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler from geventwebsocket.handler import WebSocketHandler
server = WSGIServer((host, port), self._app, server = WSGIServer(
handler_class=WebSocketHandler) (host, port), self._app, handler_class=WebSocketHandler
)
server.serve_forever() server.serve_forever()
else: else:
run(self._app, host=host, port=port) run(self._app, host=host, port=port)
except IOError as e: except IOError as e:
logger.exception("unable to start the server instance on %s:%d", logger.exception("unable to start the server instance on %s:%d", host, port)
host, port)
if e.args[0] == errno.EADDRINUSE: if e.args[0] == errno.EADDRINUSE:
raise MoulinetteError('server_already_running') raise MoulinetteError('server_already_running')
raise MoulinetteError('error_see_log') raise MoulinetteError('error_see_log')

View file

@ -15,7 +15,9 @@ import argcomplete
from moulinette import msignals, m18n from moulinette import msignals, m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.interfaces import ( from moulinette.interfaces import (
BaseActionsMapParser, BaseInterface, ExtendedArgumentParser, BaseActionsMapParser,
BaseInterface,
ExtendedArgumentParser,
) )
from moulinette.utils import log from moulinette.utils import log
@ -175,6 +177,7 @@ def get_locale():
# CLI Classes Implementation ------------------------------------------- # CLI Classes Implementation -------------------------------------------
class TTYHandler(logging.StreamHandler): class TTYHandler(logging.StreamHandler):
"""TTY log handler """TTY log handler
@ -192,6 +195,7 @@ class TTYHandler(logging.StreamHandler):
stderr. Otherwise, they are sent to stdout. stderr. Otherwise, they are sent to stdout.
""" """
LEVELS_COLOR = { LEVELS_COLOR = {
log.NOTSET: 'white', log.NOTSET: 'white',
log.DEBUG: 'white', log.DEBUG: 'white',
@ -218,8 +222,7 @@ class TTYHandler(logging.StreamHandler):
# add translated level name before message # add translated level name before message
level = '%s ' % m18n.g(record.levelname.lower()) level = '%s ' % m18n.g(record.levelname.lower())
color = self.LEVELS_COLOR.get(record.levelno, 'white') color = self.LEVELS_COLOR.get(record.levelno, 'white')
msg = '{0}{1}{2}{3}'.format( msg = '{0}{1}{2}{3}'.format(colors_codes[color], level, END_CLI_COLOR, msg)
colors_codes[color], level, END_CLI_COLOR, msg)
if self.formatter: if self.formatter:
# use user-defined formatter # use user-defined formatter
record.__dict__[self.message_key] = msg record.__dict__[self.message_key] = msg
@ -256,8 +259,9 @@ class ActionsMapParser(BaseActionsMapParser):
""" """
def __init__(self, parent=None, parser=None, subparser_kwargs=None, def __init__(
top_parser=None, **kwargs): self, parent=None, parser=None, subparser_kwargs=None, top_parser=None, **kwargs
):
super(ActionsMapParser, self).__init__(parent) super(ActionsMapParser, self).__init__(parent)
if subparser_kwargs is None: if subparser_kwargs is None:
@ -300,13 +304,10 @@ class ActionsMapParser(BaseActionsMapParser):
A new ActionsMapParser object for the category A new ActionsMapParser object for the category
""" """
parser = self._subparsers.add_parser(name, parser = self._subparsers.add_parser(
description=category_help, name, description=category_help, help=category_help, **kwargs
help=category_help, )
**kwargs) return self.__class__(self, parser, {'title': "subcommands", 'required': True})
return self.__class__(self, parser, {
'title': "subcommands", 'required': True
})
def add_subcategory_parser(self, name, subcategory_help=None, **kwargs): def add_subcategory_parser(self, name, subcategory_help=None, **kwargs):
"""Add a parser for a subcategory """Add a parser for a subcategory
@ -318,17 +319,24 @@ class ActionsMapParser(BaseActionsMapParser):
A new ActionsMapParser object for the category A new ActionsMapParser object for the category
""" """
parser = self._subparsers.add_parser(name, parser = self._subparsers.add_parser(
name,
type_="subcategory", type_="subcategory",
description=subcategory_help, description=subcategory_help,
help=subcategory_help, help=subcategory_help,
**kwargs) **kwargs
return self.__class__(self, parser, { )
'title': "actions", 'required': True return self.__class__(self, parser, {'title': "actions", 'required': True})
})
def add_action_parser(self, name, tid, action_help=None, deprecated=False, def add_action_parser(
deprecated_alias=[], **kwargs): self,
name,
tid,
action_help=None,
deprecated=False,
deprecated_alias=[],
**kwargs
):
"""Add a parser for an action """Add a parser for an action
Keyword arguments: Keyword arguments:
@ -340,18 +348,21 @@ class ActionsMapParser(BaseActionsMapParser):
A new ExtendedArgumentParser object for the action A new ExtendedArgumentParser object for the action
""" """
return self._subparsers.add_parser(name, return self._subparsers.add_parser(
name,
type_="action", type_="action",
help=action_help, help=action_help,
description=action_help, description=action_help,
deprecated=deprecated, deprecated=deprecated,
deprecated_alias=deprecated_alias) deprecated_alias=deprecated_alias,
)
def add_global_arguments(self, arguments): def add_global_arguments(self, arguments):
for argument_name, argument_options in arguments.items(): for argument_name, argument_options in arguments.items():
# will adapt arguments name for cli or api context # will adapt arguments name for cli or api context
names = self.format_arg_names(str(argument_name), names = self.format_arg_names(
argument_options.pop('full', None)) str(argument_name), argument_options.pop('full', None)
)
self.global_parser.add_argument(*names, **argument_options) self.global_parser.add_argument(*names, **argument_options)
@ -417,8 +428,7 @@ class Interface(BaseInterface):
# Set handler for authentication # Set handler for authentication
if password: if password:
msignals.set_handler('authenticate', msignals.set_handler('authenticate', lambda a, h: a(password=password))
lambda a, h: a(password=password))
try: try:
ret = self.actionsmap.process(args, timeout=timeout) ret = self.actionsmap.process(args, timeout=timeout)
@ -433,6 +443,7 @@ class Interface(BaseInterface):
if output_as == 'json': if output_as == 'json':
import json import json
from moulinette.utils.serialize import JSONExtendedEncoder from moulinette.utils.serialize import JSONExtendedEncoder
print(json.dumps(ret, cls=JSONExtendedEncoder)) print(json.dumps(ret, cls=JSONExtendedEncoder))
else: else:
plain_print_dict(ret) plain_print_dict(ret)
@ -451,8 +462,7 @@ class Interface(BaseInterface):
""" """
# TODO: Allow token authentication? # TODO: Allow token authentication?
msg = m18n.n(help) if help else m18n.g('password') msg = m18n.n(help) if help else m18n.g('password')
return authenticator(password=self._do_prompt(msg, True, False, return authenticator(password=self._do_prompt(msg, True, False, color='yellow'))
color='yellow'))
def _do_prompt(self, message, is_password, confirm, color='blue'): def _do_prompt(self, message, is_password, confirm, color='blue'):
"""Prompt for a value """Prompt for a value
@ -464,8 +474,7 @@ class Interface(BaseInterface):
""" """
if is_password: if is_password:
prompt = lambda m: getpass.getpass(colorize(m18n.g('colon', m), prompt = lambda m: getpass.getpass(colorize(m18n.g('colon', m), color))
color))
else: else:
prompt = lambda m: raw_input(colorize(m18n.g('colon', m), color)) prompt = lambda m: raw_input(colorize(m18n.g('colon', m), color))
value = prompt(message) value = prompt(message)

View file

@ -22,7 +22,10 @@ def read_file(file_path):
Keyword argument: Keyword argument:
file_path -- Path to the text file file_path -- Path to the text file
""" """
assert isinstance(file_path, basestring), "Error: file_path '%s' should be a string but is of type '%s' instead" % (file_path, type(file_path)) assert isinstance(file_path, basestring), (
"Error: file_path '%s' should be a string but is of type '%s' instead"
% (file_path, type(file_path))
)
# Check file exists # Check file exists
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
@ -35,8 +38,9 @@ def read_file(file_path):
except IOError as e: except IOError as e:
raise MoulinetteError('cannot_open_file', file=file_path, error=str(e)) raise MoulinetteError('cannot_open_file', file=file_path, error=str(e))
except Exception: except Exception:
raise MoulinetteError('unknown_error_reading_file', raise MoulinetteError(
file=file_path, error=str(e)) 'unknown_error_reading_file', file=file_path, error=str(e)
)
return file_content return file_content
@ -96,9 +100,9 @@ def read_toml(file_path):
try: try:
loaded_toml = toml.loads(file_content, _dict=OrderedDict) loaded_toml = toml.loads(file_content, _dict=OrderedDict)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EINVAL, raise MoulinetteError(
m18n.g('corrupted_toml', errno.EINVAL, m18n.g('corrupted_toml', ressource=file_path, error=str(e))
ressource=file_path, error=str(e))) )
return loaded_toml return loaded_toml
@ -131,8 +135,9 @@ def read_ldif(file_path, filtred_entries=[]):
except IOError as e: except IOError as e:
raise MoulinetteError('cannot_open_file', file=file_path, error=str(e)) raise MoulinetteError('cannot_open_file', file=file_path, error=str(e))
except Exception as e: except Exception as e:
raise MoulinetteError('unknown_error_reading_file', raise MoulinetteError(
file=file_path, error=str(e)) 'unknown_error_reading_file', file=file_path, error=str(e)
)
return parser.all_records return parser.all_records
@ -148,14 +153,25 @@ def write_to_file(file_path, data, file_mode="w"):
file_mode -- Mode used when writing the file. Option meant to be used file_mode -- Mode used when writing the file. Option meant to be used
by append_to_file to avoid duplicating the code of this function. by append_to_file to avoid duplicating the code of this function.
""" """
assert isinstance(data, basestring) or isinstance(data, list), "Error: data '%s' should be either a string or a list but is of type '%s'" % (data, type(data)) assert isinstance(data, basestring) or isinstance(data, list), (
assert not os.path.isdir(file_path), "Error: file_path '%s' point to a dir, it should be a file" % file_path "Error: data '%s' should be either a string or a list but is of type '%s'"
assert os.path.isdir(os.path.dirname(file_path)), "Error: the path ('%s') base dir ('%s') is not a dir" % (file_path, os.path.dirname(file_path)) % (data, type(data))
)
assert not os.path.isdir(file_path), (
"Error: file_path '%s' point to a dir, it should be a file" % file_path
)
assert os.path.isdir(os.path.dirname(file_path)), (
"Error: the path ('%s') base dir ('%s') is not a dir"
% (file_path, os.path.dirname(file_path))
)
# If data is a list, check elements are strings and build a single string # If data is a list, check elements are strings and build a single string
if not isinstance(data, basestring): if not isinstance(data, basestring):
for element in data: for element in data:
assert isinstance(element, basestring), "Error: element '%s' should be a string but is of type '%s' instead" % (element, type(element)) assert isinstance(element, basestring), (
"Error: element '%s' should be a string but is of type '%s' instead"
% (element, type(element))
)
data = '\n'.join(data) data = '\n'.join(data)
try: try:
@ -189,10 +205,21 @@ def write_to_json(file_path, data):
""" """
# Assumptions # Assumptions
assert isinstance(file_path, basestring), "Error: file_path '%s' should be a string but is of type '%s' instead" % (file_path, type(file_path)) assert isinstance(file_path, basestring), (
assert isinstance(data, dict) or isinstance(data, list), "Error: data '%s' should be a dict or a list but is of type '%s' instead" % (data, type(data)) "Error: file_path '%s' should be a string but is of type '%s' instead"
assert not os.path.isdir(file_path), "Error: file_path '%s' point to a dir, it should be a file" % file_path % (file_path, type(file_path))
assert os.path.isdir(os.path.dirname(file_path)), "Error: the path ('%s') base dir ('%s') is not a dir" % (file_path, os.path.dirname(file_path)) )
assert isinstance(data, dict) or isinstance(data, list), (
"Error: data '%s' should be a dict or a list but is of type '%s' instead"
% (data, type(data))
)
assert not os.path.isdir(file_path), (
"Error: file_path '%s' point to a dir, it should be a file" % file_path
)
assert os.path.isdir(os.path.dirname(file_path)), (
"Error: the path ('%s') base dir ('%s') is not a dir"
% (file_path, os.path.dirname(file_path))
)
# Write dict to file # Write dict to file
try: try:
@ -310,7 +337,9 @@ def chown(path, uid=None, gid=None, recursive=False):
for f in files: for f in files:
os.chown(os.path.join(root, f), uid, gid) os.chown(os.path.join(root, f), uid, gid)
except Exception as e: except Exception as e:
raise MoulinetteError('error_changing_file_permissions', path=path, error=str(e)) raise MoulinetteError(
'error_changing_file_permissions', path=path, error=str(e)
)
def chmod(path, mode, fmode=None, recursive=False): def chmod(path, mode, fmode=None, recursive=False):
@ -334,7 +363,9 @@ def chmod(path, mode, fmode=None, recursive=False):
for f in files: for f in files:
os.chmod(os.path.join(root, f), fmode) os.chmod(os.path.join(root, f), fmode)
except Exception as e: except Exception as e:
raise MoulinetteError('error_changing_file_permissions', path=path, error=str(e)) raise MoulinetteError(
'error_changing_file_permissions', path=path, error=str(e)
)
def rm(path, recursive=False, force=False): def rm(path, recursive=False, force=False):

View file

@ -3,8 +3,18 @@ import logging
# import all constants because other modules try to import them from this # import all constants because other modules try to import them from this
# module because SUCCESS is defined in this module # module because SUCCESS is defined in this module
from logging import (addLevelName, setLoggerClass, Logger, getLogger, NOTSET, # noqa from logging import ( # noqa
DEBUG, INFO, WARNING, ERROR, CRITICAL) addLevelName,
setLoggerClass,
Logger,
getLogger,
NOTSET,
DEBUG,
INFO,
WARNING,
ERROR,
CRITICAL,
)
# Global configuration and functions ----------------------------------- # Global configuration and functions -----------------------------------
@ -15,9 +25,7 @@ DEFAULT_LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'formatters': { 'formatters': {
'simple': { 'simple': {'format': '%(asctime)-15s %(levelname)-8s %(name)s - %(message)s'}
'format': '%(asctime)-15s %(levelname)-8s %(name)s - %(message)s'
},
}, },
'handlers': { 'handlers': {
'console': { 'console': {
@ -25,14 +33,9 @@ DEFAULT_LOGGING = {
'formatter': 'simple', 'formatter': 'simple',
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'stream': 'ext://sys.stdout', 'stream': 'ext://sys.stdout',
}
}, },
}, 'loggers': {'moulinette': {'level': 'DEBUG', 'handlers': ['console']}},
'loggers': {
'moulinette': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
} }
@ -65,7 +68,7 @@ def getHandlersByClass(classinfo, limit=0):
return o return o
handlers.append(o) handlers.append(o)
if limit != 0 and len(handlers) > limit: if limit != 0 and len(handlers) > limit:
return handlers[:limit - 1] return handlers[: limit - 1]
return handlers return handlers
@ -79,6 +82,7 @@ class MoulinetteLogger(Logger):
LogRecord extra and can be used with the ActionFilter. LogRecord extra and can be used with the ActionFilter.
""" """
action_id = None action_id = None
def success(self, msg, *args, **kwargs): def success(self, msg, *args, **kwargs):

View file

@ -15,6 +15,7 @@ def download_text(url, timeout=30, expected_status_code=200):
None to ignore the status code. None to ignore the status code.
""" """
import requests # lazy loading this module for performance reasons import requests # lazy loading this module for performance reasons
# Assumptions # Assumptions
assert isinstance(url, str) assert isinstance(url, str)
@ -32,13 +33,12 @@ def download_text(url, timeout=30, expected_status_code=200):
raise MoulinetteError('download_timeout', url=url) raise MoulinetteError('download_timeout', url=url)
# Unknown stuff # Unknown stuff
except Exception as e: except Exception as e:
raise MoulinetteError('download_unknown_error', raise MoulinetteError('download_unknown_error', url=url, error=str(e))
url=url, error=str(e))
# Assume error if status code is not 200 (OK) # Assume error if status code is not 200 (OK)
if expected_status_code is not None \ if expected_status_code is not None and r.status_code != expected_status_code:
and r.status_code != expected_status_code: raise MoulinetteError(
raise MoulinetteError('download_bad_status_code', 'download_bad_status_code', url=url, code=str(r.status_code)
url=url, code=str(r.status_code)) )
return r.text return r.text

View file

@ -11,6 +11,7 @@ except ImportError:
from shlex import quote # Python3 >= 3.3 from shlex import quote # Python3 >= 3.3
from .stream import async_file_reading from .stream import async_file_reading
quote # This line is here to avoid W0611 PEP8 error (see comments above) quote # This line is here to avoid W0611 PEP8 error (see comments above)
# Prevent to import subprocess only for common classes # Prevent to import subprocess only for common classes
@ -19,6 +20,7 @@ CalledProcessError = subprocess.CalledProcessError
# Alternative subprocess methods --------------------------------------- # Alternative subprocess methods ---------------------------------------
def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs): def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs):
"""Run command with arguments and return its output as a byte string """Run command with arguments and return its output as a byte string
@ -31,6 +33,7 @@ def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs):
# Call with stream access ---------------------------------------------- # Call with stream access ----------------------------------------------
def call_async_output(args, callback, **kwargs): def call_async_output(args, callback, **kwargs):
"""Run command and provide its output asynchronously """Run command and provide its output asynchronously
@ -54,8 +57,7 @@ def call_async_output(args, callback, **kwargs):
""" """
for a in ['stdout', 'stderr']: for a in ['stdout', 'stderr']:
if a in kwargs: if a in kwargs:
raise ValueError('%s argument not allowed, ' raise ValueError('%s argument not allowed, ' 'it will be overridden.' % a)
'it will be overridden.' % a)
if "stdinfo" in kwargs and kwargs["stdinfo"] is not None: if "stdinfo" in kwargs and kwargs["stdinfo"] is not None:
assert len(callback) == 3 assert len(callback) == 3
@ -101,7 +103,7 @@ def call_async_output(args, callback, **kwargs):
stderr_consum.process_next_line() stderr_consum.process_next_line()
if stdinfo: if stdinfo:
stdinfo_consum.process_next_line() stdinfo_consum.process_next_line()
time.sleep(.1) time.sleep(0.1)
stderr_reader.join() stderr_reader.join()
# clear the queues # clear the queues
stdout_consum.process_current_queue() stdout_consum.process_current_queue()
@ -111,7 +113,7 @@ def call_async_output(args, callback, **kwargs):
else: else:
while not stdout_reader.eof(): while not stdout_reader.eof():
stdout_consum.process_current_queue() stdout_consum.process_current_queue()
time.sleep(.1) time.sleep(0.1)
stdout_reader.join() stdout_reader.join()
# clear the queue # clear the queue
stdout_consum.process_current_queue() stdout_consum.process_current_queue()
@ -131,15 +133,15 @@ def call_async_output(args, callback, **kwargs):
while time.time() - start < 10: while time.time() - start < 10:
if p.poll() is not None: if p.poll() is not None:
return p.poll() return p.poll()
time.sleep(.1) time.sleep(0.1)
return p.poll() return p.poll()
# Call multiple commands ----------------------------------------------- # Call multiple commands -----------------------------------------------
def run_commands(cmds, callback=None, separate_stderr=False, shell=True,
**kwargs): def run_commands(cmds, callback=None, separate_stderr=False, shell=True, **kwargs):
"""Run multiple commands with error management """Run multiple commands with error management
Run a list of commands and allow to manage how to treat errors either Run a list of commands and allow to manage how to treat errors either
@ -178,14 +180,14 @@ def run_commands(cmds, callback=None, separate_stderr=False, shell=True,
# overriden by user input # overriden by user input
for a in ['stdout', 'stderr']: for a in ['stdout', 'stderr']:
if a in kwargs: if a in kwargs:
raise ValueError('%s argument not allowed, ' raise ValueError('%s argument not allowed, ' 'it will be overridden.' % a)
'it will be overridden.' % a)
# If no callback specified... # If no callback specified...
if callback is None: if callback is None:
# Raise CalledProcessError on command failure # Raise CalledProcessError on command failure
def callback(r, c, o): def callback(r, c, o):
raise CalledProcessError(r, c, o) raise CalledProcessError(r, c, o)
elif not callable(callback): elif not callable(callback):
raise ValueError('callback argument must be callable') raise ValueError('callback argument must be callable')
@ -201,8 +203,9 @@ def run_commands(cmds, callback=None, separate_stderr=False, shell=True,
error = 0 error = 0
for cmd in cmds: for cmd in cmds:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, process = subprocess.Popen(
stderr=_stderr, shell=shell, **kwargs) cmd, stdout=subprocess.PIPE, stderr=_stderr, shell=shell, **kwargs
)
output = _get_output(*process.communicate()) output = _get_output(*process.communicate())
retcode = process.poll() retcode = process.poll()

View file

@ -8,6 +8,7 @@ logger = logging.getLogger('moulinette.utils.serialize')
# JSON utilities ------------------------------------------------------- # JSON utilities -------------------------------------------------------
class JSONExtendedEncoder(JSONEncoder): class JSONExtendedEncoder(JSONEncoder):
"""Extended JSON encoder """Extended JSON encoder
@ -24,8 +25,7 @@ class JSONExtendedEncoder(JSONEncoder):
def default(self, o): def default(self, o):
"""Return a serializable object""" """Return a serializable object"""
# Convert compatible containers into list # Convert compatible containers into list
if isinstance(o, set) or ( if isinstance(o, set) or (hasattr(o, '__iter__') and hasattr(o, 'next')):
hasattr(o, '__iter__') and hasattr(o, 'next')):
return list(o) return list(o)
# Display the date in its iso format ISO-8601 Internet Profile (RFC 3339) # Display the date in its iso format ISO-8601 Internet Profile (RFC 3339)
@ -35,6 +35,9 @@ class JSONExtendedEncoder(JSONEncoder):
return o.isoformat() return o.isoformat()
# Return the repr for object that json can't encode # Return the repr for object that json can't encode
logger.warning('cannot properly encode in JSON the object %s, ' logger.warning(
'returned repr is: %r', type(o), o) 'cannot properly encode in JSON the object %s, ' 'returned repr is: %r',
type(o),
o,
)
return repr(o) return repr(o)

View file

@ -7,6 +7,7 @@ from multiprocessing.queues import SimpleQueue
# Read from a stream --------------------------------------------------- # Read from a stream ---------------------------------------------------
class AsynchronousFileReader(Process): class AsynchronousFileReader(Process):
""" """
@ -75,7 +76,6 @@ class AsynchronousFileReader(Process):
class Consummer(object): class Consummer(object):
def __init__(self, queue, callback): def __init__(self, queue, callback):
self.queue = queue self.queue = queue
self.callback = callback self.callback = callback

View file

@ -6,6 +6,7 @@ import binascii
# Pattern searching ---------------------------------------------------- # Pattern searching ----------------------------------------------------
def search(pattern, text, count=0, flags=0): def search(pattern, text, count=0, flags=0):
"""Search for pattern in a text """Search for pattern in a text
@ -55,6 +56,7 @@ def searchf(pattern, path, count=0, flags=re.MULTILINE):
# Text formatting ------------------------------------------------------ # Text formatting ------------------------------------------------------
def prependlines(text, prepend): def prependlines(text, prepend):
"""Prepend a string to each line of a text""" """Prepend a string to each line of a text"""
lines = text.splitlines(True) lines = text.splitlines(True)
@ -63,6 +65,7 @@ def prependlines(text, prepend):
# Randomize ------------------------------------------------------------ # Randomize ------------------------------------------------------------
def random_ascii(length=20): def random_ascii(length=20):
"""Return a random ascii string""" """Return a random ascii string"""
return binascii.hexlify(os.urandom(length)).decode('ascii') return binascii.hexlify(os.urandom(length)).decode('ascii')

2
pyproject.toml Normal file
View file

@ -0,0 +1,2 @@
[tool.black]
skip-string-normalization = true

View file

@ -1,2 +1,8 @@
[flake8] [flake8]
ignore = E501,E128,E731,E722 ignore =
E501,
E128,
E731,
E722,
W503 # Black formatter conflict
E203 # Black formatter conflict

View file

@ -48,47 +48,31 @@ def patch_logging(moulinette):
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': True,
'formatters': { 'formatters': {
'tty-debug': { 'tty-debug': {'format': '%(relativeCreated)-4d %(fmessage)s'},
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': { 'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' # noqa 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' # noqa
}, },
}, },
'filters': { 'filters': {'action': {'()': 'moulinette.utils.log.ActionFilter'}},
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': { 'handlers': {
'tty': { 'tty': {
'level': tty_level, 'level': tty_level,
'class': 'moulinette.interfaces.cli.TTYHandler', 'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': '', 'formatter': '',
}, }
}, },
'loggers': { 'loggers': {
'moulinette': { 'moulinette': {'level': level, 'handlers': [], 'propagate': True},
'level': level,
'handlers': [],
'propagate': True,
},
'moulinette.interface': { 'moulinette.interface': {
'level': level, 'level': level,
'handlers': handlers, 'handlers': handlers,
'propagate': False, 'propagate': False,
}, },
}, },
'root': { 'root': {'level': level, 'handlers': root_handlers},
'level': level,
'handlers': root_handlers,
},
} }
moulinette.init( moulinette.init(logging_config=logging, _from_source=False)
logging_config=logging,
_from_source=False
)
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)

View file

@ -5,7 +5,7 @@ from moulinette.actionsmap import (
AskParameter, AskParameter,
PatternParameter, PatternParameter,
RequiredParameter, RequiredParameter,
ActionsMap ActionsMap,
) )
from moulinette.interfaces import BaseActionsMapParser from moulinette.interfaces import BaseActionsMapParser
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
@ -58,11 +58,9 @@ def test_pattern_parameter_bad_str_value(iface, caplog):
assert any('expecting a list' in message for message in caplog.messages) assert any('expecting a list' in message for message in caplog.messages)
@pytest.mark.parametrize('iface', [ @pytest.mark.parametrize(
[], 'iface', [[], ['pattern_alone'], ['pattern', 'message', 'extra stuff']]
['pattern_alone'], )
['pattern', 'message', 'extra stuff']
])
def test_pattern_parameter_bad_list_len(iface): def test_pattern_parameter_bad_list_len(iface):
pattern = PatternParameter(iface) pattern = PatternParameter(iface)
with pytest.raises(TypeError): with pytest.raises(TypeError):

View file

@ -4,8 +4,14 @@ import pytest
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.filesystem import (append_to_file, read_file, read_json, from moulinette.utils.filesystem import (
rm, write_to_file, write_to_json) append_to_file,
read_file,
read_json,
rm,
write_to_file,
write_to_json,
)
def test_read_file(test_file): def test_read_file(test_file):

15
tox.ini
View file

@ -2,6 +2,7 @@
envlist = envlist =
py27 py27
lint lint
format-check
docs docs
skipdist = True skipdist = True
@ -24,6 +25,20 @@ deps = flake8
skip_install = True skip_install = True
usedevelop = False usedevelop = False
[testenv:format]
basepython = python3
commands = black {posargs} moulinette test
deps = black
skip_install = True
usedevelop = False
[testenv:format-check]
basepython = {[testenv:format]basepython}
commands = black {posargs:--check --diff} moulinette test
deps = {[testenv:format]deps}
skip_install = {[testenv:format]skip_install}
usedevelop = {[testenv:format]usedevelop}
[testenv:docs] [testenv:docs]
usedevelop = True usedevelop = True
commands = python -m sphinx -W doc/ doc/_build commands = python -m sphinx -W doc/ doc/_build