Try to improve time execution and continue refactoring

* Revert to classes centralization into actionsmap.py
* Try to optimize conditions and loops
* Revisit Package class and get directories from a file generated at build
* Early refactoring of i18n
* Move yunohost library into /lib
This commit is contained in:
Jerome Lebleu 2014-03-11 00:57:28 +01:00
parent 9104024fa1
commit 9c9ccc1271
27 changed files with 660 additions and 632 deletions

View file

@ -3,25 +3,21 @@
import sys import sys
import os.path import os.path
import gettext
# Run from source # Run from source
basedir = os.path.abspath(os.path.dirname(__file__) +'/../') basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir(basedir +'/src'): if os.path.isdir('%s/src' % basedir):
sys.path.append(basedir +'/src') sys.path.append('%s/src' % basedir)
from moulinette import init, cli, MoulinetteError from moulinette import init, cli, MoulinetteError
from moulinette.helpers import YunoHostError, colorize from moulinette.helpers import YunoHostError, colorize
gettext.install('yunohost')
## Main action ## Main action
if __name__ == '__main__': if __name__ == '__main__':
# Run from source (prefix and libdir set to None) # Run from source
init('yunohost', prefix=None, libdir=None, init(_from_source=True)
cachedir=os.path.join(basedir, 'cache'))
# Additional arguments # Additional arguments
use_cache = True use_cache = True
@ -39,7 +35,7 @@ if __name__ == '__main__':
raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'")) raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'"))
# Execute the action # Execute the action
cli(args, use_cache) cli(['yunohost'], args, use_cache)
except MoulinetteError as e: except MoulinetteError as e:
print(e.colorize()) print(e.colorize())
sys.exit(e.code) sys.exit(e.code)

View file

@ -3,7 +3,6 @@
import sys import sys
import os.path import os.path
import gettext
# Run from source # Run from source
basedir = os.path.abspath(os.path.dirname(__file__) +'/../') basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
@ -12,8 +11,6 @@ if os.path.isdir(basedir +'/src'):
from moulinette import init, api from moulinette import init, api
gettext.install('yunohost')
## Callbacks for additional routes ## Callbacks for additional routes
@ -31,9 +28,8 @@ def is_installed():
## Main action ## Main action
if __name__ == '__main__': if __name__ == '__main__':
# Run from source (prefix and libdir set to None) # Run from source
init('yunohost', prefix=None, libdir=None, init(_from_source=True)
cachedir=os.path.join(basedir, 'cache'))
# Additional arguments # Additional arguments
use_cache = True use_cache = True
@ -45,5 +41,5 @@ if __name__ == '__main__':
# TODO: Add log argument # TODO: Add log argument
# Rune the server # Rune the server
api(6787, {('GET', '/installed'): is_installed}, use_cache) api(['yunohost'], 6787, {('GET', '/installed'): is_installed}, use_cache)
sys.exit(0) sys.exit(0)

View file

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
__title__ = 'moulinette' __title__ = 'moulinette'
__version__ = '695' __version__ = '0.1'
__author__ = ['Kload', __author__ = ['Kload',
'jlebleu', 'jlebleu',
'titoko', 'titoko',
@ -31,12 +31,10 @@ __all__ = [
from .core import MoulinetteError from .core import MoulinetteError
curr_namespace = None
## Package functions ## Package functions
def init(namespace=None, **kwargs): def init(**kwargs):
"""Package initialization """Package initialization
Initialize directories and global variables. It must be called Initialize directories and global variables. It must be called
@ -44,30 +42,33 @@ def init(namespace=None, **kwargs):
functions. functions.
Keyword arguments: Keyword arguments:
- namespace -- The namespace to initialize and use - **kwargs -- See core.Package
- **kwargs -- See helpers.Package
At the end, the global variable 'pkg' will contain a Package At the end, the global variable 'pkg' will contain a Package
instance. See helpers.Package for available methods and variables. instance. See core.Package for available methods and variables.
""" """
import sys
import __builtin__ import __builtin__
from .core import Package from .core import Package, install_i18n
global curr_namespace
curr_namespace = namespace
__builtin__.__dict__['pkg'] = Package(**kwargs) __builtin__.__dict__['pkg'] = Package(**kwargs)
# Initialize internationalization
install_i18n()
# Add library directory to python path
sys.path.append(pkg.libdir)
## Easy access to interfaces ## Easy access to interfaces
def api(port, routes={}, use_cache=True): def api(namespaces, port, routes={}, 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.
Keyword arguments: Keyword arguments:
- namespaces -- The list of namespaces to use
- port -- Port to run on - port -- Port to run on
- routes -- A dict of additional routes to add in the form of - routes -- A dict of additional routes to add in the form of
{(method, uri): callback} {(method, uri): callback}
@ -79,18 +80,19 @@ def api(port, routes={}, use_cache=True):
from .actionsmap import ActionsMap from .actionsmap import ActionsMap
from .interface.api import MoulinetteAPI from .interface.api import MoulinetteAPI
amap = ActionsMap('api', use_cache=use_cache) amap = ActionsMap('api', namespaces, use_cache)
moulinette = MoulinetteAPI(amap, routes) moulinette = MoulinetteAPI(amap, routes)
run(moulinette.app, port=port) run(moulinette.app, port=port)
def cli(args, use_cache=True): def cli(namespaces, args, use_cache=True):
"""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
result in a readable format. result in a readable format.
Keyword arguments: Keyword arguments:
- namespaces -- The list of namespaces to use
- args -- A list of argument strings - args -- A list of argument strings
- use_cache -- False if it should parse the actions map file - use_cache -- False if it should parse the actions map file
instead of using the cached one instead of using the cached one
@ -98,7 +100,7 @@ def cli(args, use_cache=True):
""" """
import os import os
from .actionsmap import ActionsMap from .actionsmap import ActionsMap
from .helpers import YunoHostError, pretty_print_dict from .helpers import pretty_print_dict
lock_file = '/var/run/moulinette.lock' lock_file = '/var/run/moulinette.lock'
@ -112,7 +114,7 @@ def cli(args, use_cache=True):
os.system('chmod 400 '+ lock_file) os.system('chmod 400 '+ lock_file)
try: try:
amap = ActionsMap('cli', use_cache=use_cache) amap = ActionsMap('cli', namespaces, use_cache)
pretty_print_dict(amap.process(args)) pretty_print_dict(amap.process(args))
except KeyboardInterrupt, EOFError: except KeyboardInterrupt, EOFError:
raise MoulinetteError(125, _("Interrupted")) raise MoulinetteError(125, _("Interrupted"))

View file

@ -1,21 +1,478 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pickle import argparse
import yaml import yaml
import re import re
import os import os
import cPickle as pickle
from collections import OrderedDict from collections import OrderedDict
import logging import logging
from . import __version__, curr_namespace, MoulinetteError from . import __version__
from .extra.parameters import extraparameters_list from .core import MoulinetteError
## Extra parameters Parser ## Interfaces' Actions map Parser --------------------------------------
class _AMapParser(object):
"""Actions map's base Parser
Each interfaces must implement a parser class derived from this
class. It is used to parse the main parts of the actions map (i.e.
general arguments, categories and actions).
class ExtraParser(object):
""" """
Global parser for the extra parameters.
## Optional variables
# Each parser classes can overwrite these variables.
"""Either it will parse general arguments, or not"""
parse_general_arguments = True
## Virtual methods
# Each parser classes can implement these methods.
@staticmethod
def format_arg_name(name, full):
"""Format argument name
Format agument name depending on its 'full' parameters and return
a list to use it as option string for the argument parser.
Keyword arguments:
- name -- The argument name
- full -- The argument's 'full' parameter
Returns:
A list of option strings
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_general_parser(self, **kwargs):
"""Add a parser for general arguments
Create and return an argument parser for general arguments.
Returns:
An ArgumentParser based object
"""
if not self.parse_general_arguments:
msg = "doesn't parse general arguments"
else:
msg = "must override this method"
raise NotImplementedError("derived class '%s' %s" % \
(self.__class__.__name__, msg))
def add_category_parser(self, name, **kwargs):
"""Add a parser for a category
Create a new category and return a parser for it.
Keyword arguments:
- name -- The category name
Returns:
A BaseParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_action_parser(self, name, **kwargs):
"""Add a parser for an action
Create a new action and return an argument parser for it.
Keyword arguments:
- name -- The action name
Returns:
An ArgumentParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def parse_args(self, args, **kwargs):
"""Parse arguments
Convert argument variables to objects and assign them as
attributes of the namespace.
Keyword arguments:
- args -- Arguments string or dict (TODO)
Returns:
The populated namespace
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
# CLI Actions map Parser
class CLIAMapParser(_AMapParser):
"""Actions map's CLI Parser
"""
def __init__(self, parser=None):
self._parser = parser or argparse.ArgumentParser()
self._subparsers = self._parser.add_subparsers()
@staticmethod
def format_arg_name(name, full):
if name[0] == '-' and full:
return [name, full]
return [name]
def add_general_parser(self, **kwargs):
return self._parser
def add_category_parser(self, name, category_help=None, **kwargs):
"""Add a parser for a category
Keyword arguments:
- category_help -- A brief description for the category
Returns:
A new CLIParser object for the category
"""
parser = self._subparsers.add_parser(name, help=category_help)
return self.__class__(parser)
def add_action_parser(self, name, action_help, **kwargs):
"""Add a parser for an action
Keyword arguments:
- action_help -- A brief description for the action
Returns:
A new argparse.ArgumentParser object for the action
"""
return self._subparsers.add_parser(name, help=action_help)
def parse_args(self, args, **kwargs):
return self._parser.parse_args(args)
# API Actions map Parser
class _HTTPArgumentParser(object):
"""Argument parser for HTTP requests
Object for parsing HTTP requests into Python objects. It is based
on argparse.ArgumentParser class and implements some of its methods.
"""
def __init__(self):
# Initialize the ArgumentParser object
self._parser = argparse.ArgumentParser(usage='',
prefix_chars='@',
add_help=False)
self._parser.error = self._error
self._positional = [] # list(arg_name)
self._optional = {} # dict({arg_name: option_strings})
def set_defaults(self, **kwargs):
return self._parser.set_defaults(**kwargs)
def get_default(self, dest):
return self._parser.get_default(dest)
def add_argument(self, *args, **kwargs):
action = self._parser.add_argument(*args, **kwargs)
# Append newly created action
if len(action.option_strings) == 0:
self._positional.append(action.dest)
else:
self._optional[action.dest] = action.option_strings
return action
def parse_args(self, args):
arg_strings = []
## Append an argument to the current one
def append(arg_strings, value, option_string=None):
# TODO: Process list arguments
if isinstance(value, bool):
# Append the option string only
if option_string is not None:
arg_strings.append(option_string)
elif isinstance(value, str):
if option_string is not None:
arg_strings.append(option_string)
arg_strings.append(value)
else:
arg_strings.append(value)
return arg_strings
# Iterate over positional arguments
for dest in self._positional:
if dest in args:
arg_strings = append(arg_strings, args[dest])
# Iterate over optional arguments
for dest, opt in self._optional.items():
if dest in args:
arg_strings = append(arg_strings, args[dest], opt[0])
return self._parser.parse_args(arg_strings)
def _error(self, message):
# TODO: Raise a proper exception
raise MoulinetteError(1, message)
class APIAMapParser(_AMapParser):
"""Actions map's API Parser
"""
parse_general_arguments = False
def __init__(self):
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
@property
def routes(self):
"""Get current routes"""
return self._parsers.keys()
## Implement virtual methods
@staticmethod
def format_arg_name(name, full):
if name[0] != '-':
return [name]
if full:
return [full.replace('--', '@', 1)]
if name.startswith('--'):
return [name.replace('--', '@', 1)]
return [name.replace('-', '@', 1)]
def add_category_parser(self, name, **kwargs):
return self
def add_action_parser(self, name, api=None, **kwargs):
"""Add a parser for an action
Keyword arguments:
- api -- The action route (e.g. 'GET /' )
Returns:
A new _HTTPArgumentParser object for the route
"""
if not api:
return None
# Validate action route
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
if not m:
return None
# Check if a parser already exists for the route
key = (m.group(1), m.group(2))
if key in self.routes:
raise ValueError("A parser for '%s' already exists" % key)
# Create and append parser
parser = _HTTPArgumentParser()
self._parsers[key] = parser
# Return the created parser
return parser
def parse_args(self, args, route, **kwargs):
"""Parse arguments
Keyword arguments:
- route -- The action route (e.g. 'GET /' )
"""
# Retrieve the parser for the route
if route not in self.routes:
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
return self._parsers[route].parse_args(args)
"""
The dict of interfaces names and their associated parser class.
"""
actionsmap_parsers = {
'api': APIAMapParser,
'cli': CLIAMapParser
}
## Extra parameters ----------------------------------------------------
# Extra parameters definition
class _ExtraParameter(object):
"""
Argument parser for an extra parameter.
It is a pure virtual class that each extra parameter classes must
implement.
"""
def __init__(self, iface):
# TODO: Add conn argument which contains authentification object
self.iface = iface
## Required variables
# Each extra parameters classes must overwrite these variables.
"""The extra parameter name"""
name = None
## Optional variables
# Each extra parameters classes can overwrite these variables.
"""A list of interface for which the parameter doesn't apply"""
skipped_iface = {}
## Virtual methods
# Each extra parameters classes can implement these methods.
def __call__(self, parameter, arg_name, arg_value):
"""
Parse the argument
Keyword arguments:
- parameter -- The value of this parameter for the action
- arg_name -- The argument name
- arg_value -- The argument value
Returns:
The new argument value
"""
return arg_value
@staticmethod
def validate(value, arg_name):
"""
Validate the parameter value for an argument
Keyword arguments:
- value -- The parameter value
- arg_name -- The argument name
Returns:
The validated parameter value
"""
return value
class AskParameter(_ExtraParameter):
"""
Ask for the argument value if possible and needed.
The value of this parameter corresponds to the message to display
when asking the argument value.
"""
name = 'ask'
skipped_iface = { 'api' }
def __call__(self, message, arg_name, arg_value):
# TODO: Fix asked arguments ordering
if arg_value:
return arg_value
# Ask for the argument value
ret = raw_input(colorize(message + ': ', 'cyan'))
return ret
@classmethod
def validate(klass, value, arg_name):
# Allow boolean or empty string
if isinstance(value, bool) or (isinstance(value, str) and not value):
logging.debug("value of '%s' extra parameter for '%s' argument should be a string" \
% (klass.name, arg_name))
value = arg_name
elif not isinstance(value, str):
raise TypeError("Invalid type of '%s' extra parameter for '%s' argument" \
% (klass.name, arg_name))
return value
class PasswordParameter(AskParameter):
"""
Ask for the password argument value if possible and needed.
The value of this parameter corresponds to the message to display
when asking the password.
"""
name = 'password'
def __call__(self, message, arg_name, arg_value):
if arg_value:
return arg_value
# Ask for the password
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
if pwd1 != pwd2:
raise MoulinetteError(22, _("Passwords don't match"))
return pwd1
class PatternParameter(_ExtraParameter):
"""
Check if the argument value match a pattern.
The value of this parameter corresponds to a list of the pattern and
the message to display if it doesn't match.
"""
name = 'pattern'
def __call__(self, arguments, arg_name, arg_value):
pattern = arguments[0]
message = arguments[1]
if arg_value is not None and not re.match(pattern, arg_value):
raise MoulinetteError(22, message)
return arg_value
@staticmethod
def validate(value, arg_name):
# Tolerate string type
if isinstance(value, str):
logging.warning("value of 'pattern' extra parameter for '%s' argument should be a list" % arg_name)
value = [value, _("'%s' argument is not matching the pattern") % arg_name]
elif not isinstance(value, list) or len(value) != 2:
raise TypeError("Invalid type of 'pattern' extra parameter for '%s' argument" % arg_name)
return value
"""
The list of available extra parameters classes. It will keep to this list
order on argument parsing.
"""
extraparameters_list = {AskParameter, PasswordParameter, PatternParameter}
# Extra parameters argument Parser
class ExtraArgumentParser(object):
"""
Argument validator and parser for the extra parameters.
Keyword arguments:
- iface -- The running interface
""" """
def __init__(self, iface): def __init__(self, iface):
@ -78,69 +535,64 @@ class ExtraParser(object):
return arg_value return arg_value
## Main class ## Main class ----------------------------------------------------------
class ActionsMap(object): class ActionsMap(object):
""" """Validate and process actions defined into an actions map
Validate and process action defined into the actions map.
The actions map defines features and their usage of the main The actions map defines the features and their usage of the main
application. It is composed by categories which contain one or more application. It is composed by categories which contain one or more
action(s). Moreover, the action can have specific argument(s). action(s). Moreover, the action can have specific argument(s).
Keyword arguments: This class allows to manipulate one or several actions maps
associated to a namespace. If no namespace is given, it will load
all available namespaces.
- interface -- Interface type that requires the actions map. Keyword arguments:
Possible value is one of: - interface -- The type of interface which needs the actions map.
Possible values are:
- 'cli' for the command line interface - 'cli' for the command line interface
- 'api' for an API usage (HTTP requests) - 'api' for an API usage (HTTP requests)
- namespaces -- The list of namespaces to use
- use_cache -- False if it should parse the actions map file - use_cache -- False if it should parse the actions map file
instead of using the cached one. instead of using the cached one.
""" """
def __init__(self, interface, use_cache=True): def __init__(self, interface, namespaces=[], use_cache=True):
self.use_cache = use_cache self.use_cache = use_cache
try: try:
# Retrieve the interface parser # Retrieve the interface parser
mod = __import__('interface.%s' % interface, self._parser_class = actionsmap_parsers[interface]
globals=globals(), level=1, except KeyError:
fromlist=['actionsmap_parser'])
parser = getattr(mod, 'actionsmap_parser')
except (AttributeError, ImportError):
raise MoulinetteError(22, _("Invalid interface '%s'" % interface)) raise MoulinetteError(22, _("Invalid interface '%s'" % interface))
else:
self._parser_class = parser
logging.debug("initializing ActionsMap for the '%s' interface" % interface) logging.debug("initializing ActionsMap for the '%s' interface" % interface)
if len(namespaces) == 0:
namespaces = self.get_namespaces()
actionsmaps = {} actionsmaps = {}
namespaces = self.get_actionsmap_namespaces()
if curr_namespace and curr_namespace in namespaces:
namespaces = [curr_namespace]
# Iterate over actions map namespaces # Iterate over actions map namespaces
for n in namespaces: for n in namespaces:
logging.debug("loading '%s' actions map namespace" % n) logging.debug("loading '%s' actions map namespace" % n)
if use_cache: if use_cache:
# Attempt to load cache if it exists try:
cache_file = '%s/%s.pkl' % (pkg.cachedir('actionsmap'), n) # Attempt to load cache
if os.path.isfile(cache_file): with open('%s/actionsmap/%s.pkl' % (pkg.cachedir, n)) as f:
with open(cache_file, 'r') as f:
actionsmaps[n] = pickle.load(f) actionsmaps[n] = pickle.load(f)
else: # TODO: Switch to python3 and catch proper exception
except IOError:
self.use_cache = False self.use_cache = False
actionsmaps = self.generate_cache(namespaces) actionsmaps = self.generate_cache(namespaces)
break break
else: else:
am_file = '%s/%s.yml' % (pkg.datadir('actionsmap'), n) with open('%s/actionsmap/%s.yml' % (pkg.datadir, n)) as f:
with open(am_file, 'r') as f:
actionsmaps[n] = yaml.load(f) actionsmaps[n] = yaml.load(f)
# Generate parsers # Generate parsers
self.extraparser = ExtraParser(interface) self.extraparser = ExtraArgumentParser(interface)
self.parser = self._construct_parser(actionsmaps) self.parser = self._construct_parser(actionsmaps)
def process(self, args, **kwargs): def process(self, args, **kwargs):
@ -152,6 +604,8 @@ class ActionsMap(object):
- **kwargs -- Additional interface arguments - **kwargs -- Additional interface arguments
""" """
# Check moulinette status
# Parse arguments # Parse arguments
arguments = vars(self.parser.parse_args(args, **kwargs)) arguments = vars(self.parser.parse_args(args, **kwargs))
arguments = self._parse_extra_parameters(arguments) arguments = self._parse_extra_parameters(arguments)
@ -170,11 +624,12 @@ class ActionsMap(object):
else: else:
# Process the action # Process the action
return func(**arguments) return func(**arguments)
return {}
@staticmethod @staticmethod
def get_actionsmap_namespaces(): def get_namespaces():
""" """
Retrieve actions map namespaces from a given path Retrieve available actions map namespaces
Returns: Returns:
A list of available namespaces A list of available namespaces
@ -182,7 +637,7 @@ class ActionsMap(object):
""" """
namespaces = [] namespaces = []
for f in os.listdir(pkg.datadir('actionsmap')): for f in os.listdir('%s/actionsmap' % pkg.datadir):
if f.endswith('.yml'): if f.endswith('.yml'):
namespaces.append(f[:-4]) namespaces.append(f[:-4])
return namespaces return namespaces
@ -201,20 +656,19 @@ class ActionsMap(object):
""" """
actionsmaps = {} actionsmaps = {}
if not namespaces: if not namespaces:
namespaces = klass.get_actionsmap_namespaces() namespaces = klass.get_namespaces()
# Iterate over actions map namespaces # Iterate over actions map namespaces
for n in namespaces: for n in namespaces:
logging.debug("generating cache for '%s' actions map namespace" % n) logging.debug("generating cache for '%s' actions map namespace" % n)
# Read actions map from yaml file # Read actions map from yaml file
am_file = pkg.datafile('actionsmap/%s.yml' % n) am_file = '%s/actionsmap/%s.yml' % (pkg.datadir, n)
with open(am_file, 'r') as f: with open(am_file, 'r') as f:
actionsmaps[n] = yaml.load(f) actionsmaps[n] = yaml.load(f)
# Cache actions map into pickle file # Cache actions map into pickle file
cache_file = pkg.cachefile('actionsmap/%s.pkl' % n, make_dir=True) with pkg.open_cache('%s.pkl' % n, subdir='actionsmap') as f:
with open(cache_file, 'w') as f:
pickle.dump(actionsmaps[n], f) pickle.dump(actionsmaps[n], f)
return actionsmaps return actionsmaps
@ -291,41 +745,41 @@ class ActionsMap(object):
for n, actionsmap in actionsmaps.items(): for n, actionsmap in actionsmaps.items():
if 'general_arguments' in actionsmap: if 'general_arguments' in actionsmap:
# Parse general arguments # Parse general arguments
if top_parser.parse_general: if top_parser.parse_general_arguments:
parser = top_parser.add_general_parser() parser = top_parser.add_general_parser()
for an, ap in actionsmap['general_arguments'].items(): for an, ap in actionsmap['general_arguments'].items():
if 'version' in ap: # Replace version number
ap['version'] = ap['version'].replace('%version%', version = ap.get('version', None)
__version__) if version:
ap['version'] = version.replace('%version%',
__version__)
argname = top_parser.format_arg_name(an, ap.pop('full', None)) argname = top_parser.format_arg_name(an, ap.pop('full', None))
parser.add_argument(*argname, **ap) parser.add_argument(*argname, **ap)
del actionsmap['general_arguments'] del actionsmap['general_arguments']
# Parse categories # Parse categories
for cn, cp in actionsmap.items(): for cn, cp in actionsmap.items():
if 'actions' not in cp: try:
actions = cp.pop('actions')
except KeyError:
continue continue
actions = cp.pop('actions')
# Add category parser # Add category parser
if top_parser.parse_category: cat_parser = top_parser.add_category_parser(cn, **cp)
cat_parser = top_parser.add_category_parser(cn, **cp)
else:
cat_parser = top_parser
# Parse actions # Parse actions
if not top_parser.parse_action:
continue
for an, ap in actions.items(): for an, ap in actions.items():
arguments = ap.pop('arguments', {}) arguments = ap.pop('arguments', {})
# Add action parser # Add action parser
parser = cat_parser.add_action_parser(an, **ap) parser = cat_parser.add_action_parser(an, **ap)
if not parser:
continue
# Store action information try:
parser.set_defaults(_info=(n, cn, an)) # Store action information
parser.set_defaults(_info=(n, cn, an))
except AttributeError:
# No parser for the action
break
# Add action arguments # Add action arguments
for argn, argp in arguments.items(): for argn, argp in arguments.items():

View file

@ -5,115 +5,126 @@ import sys
import gettext import gettext
from .helpers import colorize from .helpers import colorize
# Package manipulation -------------------------------------------------
def install_i18n(namespace=None):
"""Install internationalization
Install translation based on the package's default gettext domain or
on 'namespace' if provided.
Keyword arguments:
- namespace -- The namespace to initialize i18n for
"""
if namespace:
try:
t = gettext.translation(namespace, pkg.localedir)
except IOError:
# TODO: Log error
return
else:
t.install()
else:
gettext.install('moulinette', pkg.localedir)
class Package(object): class Package(object):
"""Package representation and easy access """Package representation and easy access methods
Initialize directories and variables for the package and give them Initialize directories and variables for the package and give them
easy access. easy access.
Keyword arguments: Keyword arguments:
- prefix -- The installation prefix - _from_source -- Either the package is running from source or
- libdir -- The library directory; usually, this would be not (only for debugging)
prefix + '/lib' (or '/lib64') when installed
- cachedir -- The cache directory; usually, this would be
'/var/cache' when installed
- destdir -- The destination prefix only if it's an installation
'prefix' and 'libdir' arguments should be empty in order to run
package from source.
""" """
def __init__(self, prefix, libdir, cachedir, destdir=None): def __init__(self, _from_source=False):
if not prefix and not libdir: if _from_source:
# Running from source directory import sys
basedir = os.path.abspath(os.path.dirname(sys.argv[0]) +'/../') basedir = os.path.abspath(os.path.dirname(sys.argv[0]) +'/../')
self._datadir = os.path.join(basedir, 'data')
self._libdir = os.path.join(basedir, 'src') # Set local directories
self._cachedir = cachedir self._datadir = '%s/data' % basedir
self._libdir = '%s/lib' % basedir
self._localedir = '%s/po' % basedir
self._cachedir = '%s/cache' % basedir
else: else:
self._datadir = os.path.join(prefix, 'share/moulinette') import package
self._libdir = os.path.join(libdir, 'moulinette')
self._cachedir = os.path.join(cachedir, 'moulinette')
# Append library path to python's path # Set system directories
sys.path.append(self._libdir) self._datadir = package.datadir
self._destdir = destdir or None self._libdir = package.libdir
self._localedir = package.localedir
self._cachedir = package.cachedir
def __setattr__(self, name, value):
if name[0] == '_' and self.__dict__.has_key(name):
# Deny reassignation of package directories
raise TypeError("cannot reassign constant '%s'")
self.__dict__[name] = value
## Easy access to directories and files ## Easy access to package directories
def datadir(self, subdir=None, **kwargs): @property
"""Return the path to a data directory""" def datadir(self):
return self.get_dir(self._datadir, subdir, **kwargs) """Return the data directory of the package"""
return self._datadir
def datafile(self, filename, **kwargs): @property
"""Return the path to a data file""" def libdir(self):
return self.get_file(self._datadir, filename, **kwargs) """Return the lib directory of the package"""
return self._libdir
def libdir(self, subdir=None, **kwargs): @property
"""Return the path to a lib directory""" def localedir(self):
return self.get_dir(self._libdir, subdir, **kwargs) """Return the locale directory of the package"""
return self._localedir
def libfile(self, filename, **kwargs): @property
"""Return the path to a lib file""" def cachedir(self):
return self.get_file(self._libdir, filename, **kwargs) """Return the cache directory of the package"""
return self._cachedir
def cachedir(self, subdir=None, **kwargs):
"""Return the path to a cache directory"""
return self.get_dir(self._cachedir, subdir, **kwargs)
def cachefile(self, filename, **kwargs):
"""Return the path to a cache file"""
return self.get_file(self._cachedir, filename, **kwargs)
## Standard methods ## Additional methods
def get_dir(self, basedir, subdir=None, make_dir=False): def get_cachedir(self, subdir='', make_dir=True):
"""Get a directory path """Get the path to a cache directory
Return a path composed by a base directory and an optional Return the path to the cache directory from an optional
subdirectory. The path will be created if needed. subdirectory and create it if needed.
Keyword arguments: Keyword arguments:
- basedir -- The base directory - subdir -- A cache subdirectory
- subdir -- An optional subdirectory - make_dir -- False to not make directory if it not exists
- make_dir -- True if it should create needed directory
""" """
# Retrieve path path = os.path.join(self.cachedir, subdir)
path = basedir
if self._destdir:
path = os.path.join(self._destdir, path)
if subdir:
path = os.path.join(path, subdir)
# Create directory
if make_dir and not os.path.isdir(path): if make_dir and not os.path.isdir(path):
os.makedirs(path) os.makedirs(path)
return path return path
def get_file(self, basedir, filename, **kwargs): def open_cache(self, filename, subdir='', mode='w'):
"""Get a file path """Open a cache file and return a stream
Return the path of the filename in the specified directory. This Attempt to open in 'mode' the cache file 'filename' from the
directory will be created if needed. default cache directory and in the subdirectory 'subdir' if
given. Directories are created if needed and a stream is
returned if the file can be written.
Keyword arguments: Keyword arguments:
- basedir -- The base directory of the file - filename -- The cache filename
- filename -- The filename or a path relative to basedir - subdir -- A subdirectory which contains the file
- **kwargs -- Additional arguments for Package.get_dir - mode -- The mode in which the file is opened
""" """
# Check for a directory in filename return open('%s/%s' % (self.get_cachedir(subdir), filename), mode)
subdir = os.path.dirname(filename) or None
if subdir:
filename = os.path.basename(filename)
# Get directory path
dirpath = self.get_dir(basedir, subdir, **kwargs)
return os.path.join(dirpath, filename)
# Moulinette core classes ----------------------------------------------
class MoulinetteError(Exception): class MoulinetteError(Exception):
"""Moulinette base exception """Moulinette base exception

View file

@ -1,159 +0,0 @@
# -*- coding: utf-8 -*-
import getpass
import re
import logging
from .. import MoulinetteError
from ..helpers import colorize
class _ExtraParameter(object):
"""
Argument parser for an extra parameter.
It is a pure virtual class that each extra parameter classes must
implement.
"""
def __init__(self, iface):
# TODO: Add conn argument which contains authentification object
self.iface = iface
## Required variables
# Each extra parameters classes must overwrite these variables.
"""The extra parameter name"""
name = None
## Optional variables
# Each extra parameters classes can overwrite these variables.
"""A list of interface for which the parameter doesn't apply"""
skipped_iface = {}
## Virtual methods
# Each extra parameters classes can implement these methods.
def __call__(self, parameter, arg_name, arg_value):
"""
Parse the argument
Keyword arguments:
- parameter -- The value of this parameter for the action
- arg_name -- The argument name
- arg_value -- The argument value
Returns:
The new argument value
"""
return arg_value
@staticmethod
def validate(value, arg_name):
"""
Validate the parameter value for an argument
Keyword arguments:
- value -- The parameter value
- arg_name -- The argument name
Returns:
The validated parameter value
"""
return value
## Extra parameters definitions
class AskParameter(_ExtraParameter):
"""
Ask for the argument value if possible and needed.
The value of this parameter corresponds to the message to display
when asking the argument value.
"""
name = 'ask'
skipped_iface = { 'api' }
def __call__(self, message, arg_name, arg_value):
# TODO: Fix asked arguments ordering
if arg_value:
return arg_value
# Ask for the argument value
ret = raw_input(colorize(message + ': ', 'cyan'))
return ret
@classmethod
def validate(klass, value, arg_name):
# Allow boolean or empty string
if isinstance(value, bool) or (isinstance(value, str) and not value):
logging.debug("value of '%s' extra parameter for '%s' argument should be a string" \
% (klass.name, arg_name))
value = arg_name
elif not isinstance(value, str):
raise TypeError("Invalid type of '%s' extra parameter for '%s' argument" \
% (klass.name, arg_name))
return value
class PasswordParameter(AskParameter):
"""
Ask for the password argument value if possible and needed.
The value of this parameter corresponds to the message to display
when asking the password.
"""
name = 'password'
def __call__(self, message, arg_name, arg_value):
if arg_value:
return arg_value
# Ask for the password
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
if pwd1 != pwd2:
raise MoulinetteError(22, _("Passwords don't match"))
return pwd1
class PatternParameter(_ExtraParameter):
"""
Check if the argument value match a pattern.
The value of this parameter corresponds to a list of the pattern and
the message to display if it doesn't match.
"""
name = 'pattern'
def __call__(self, arguments, arg_name, arg_value):
pattern = arguments[0]
message = arguments[1]
if arg_value is not None and not re.match(pattern, arg_value):
raise MoulinetteError(22, message)
return arg_value
@staticmethod
def validate(value, arg_name):
# Tolerate string type
if isinstance(value, str):
logging.warning("value of 'pattern' extra parameter for '%s' argument should be a list" % arg_name)
value = [value, _("'%s' argument is not matching the pattern") % arg_name]
elif not isinstance(value, list) or len(value) != 2:
raise TypeError("Invalid type of 'pattern' extra parameter for '%s' argument" % arg_name)
return value
"""
The list of available extra parameters classes. It will keep to this list
order on argument parsing.
"""
extraparameters_list = {AskParameter, PasswordParameter, PatternParameter}

View file

@ -1,25 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import sys import ldap
try:
import ldap
except ImportError:
sys.stderr.write('Error: Yunohost CLI Require LDAP lib\n')
sys.stderr.write('apt-get install python-ldap\n')
sys.exit(1)
import ldap.modlist as modlist import ldap.modlist as modlist
import yaml
import json import json
import re import re
import getpass import getpass
import random import random
import string import string
import argparse
import gettext import gettext
import getpass import getpass
if not __debug__:
import traceback
win = [] win = []

114
src/moulinette/interface/__init__.py Executable file → Normal file
View file

@ -1,114 +0,0 @@
# -*- coding: utf-8 -*-
class BaseParser(object):
"""Actions map's base Parser
Each interfaces must implement a parser class derived from this
class. It is used to parse the main parts of the actions map (i.e.
general arguments, categories and actions).
"""
## Optional variables
# Each parser classes can overwrite these variables.
"""Either it will parse general arguments, or not"""
parse_general = True
"""Either it will parse categories, or not"""
parse_category = True
"""Either it will parse actions, or not"""
parse_action = True
## Virtual methods
# Each parser classes can implement these methods.
@staticmethod
def format_arg_name(name, full):
"""Format argument name
Format agument name depending on its 'full' parameters and return
a list to use it as option string for the argument parser.
Keyword arguments:
- name -- The argument name
- full -- The argument's 'full' parameter
Returns:
A list of option strings
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_general_parser(self, **kwargs):
"""Add a parser for general arguments
Create and return an argument parser for general arguments.
Returns:
An ArgumentParser based object
"""
if not self.parse_general:
msg = "doesn't parse general arguments"
else:
msg = "must override this method"
raise NotImplementedError("derived class '%s' %s" % \
(self.__class__.__name__, msg))
def add_category_parser(self, name, **kwargs):
"""Add a parser for a category
Create a new category and return a parser for it.
Keyword arguments:
- name -- The category name
Returns:
A BaseParser based object
"""
if not self.parse_categories:
msg = "doesn't parse categories"
else:
msg = "must override this method"
raise NotImplementedError("derived class '%s' %s" % \
(self.__class__.__name__, msg))
def add_action_parser(self, name, **kwargs):
"""Add a parser for an action
Create a new action and return an argument parser for it.
Keyword arguments:
- name -- The action name
Returns:
An ArgumentParser based object
"""
if not self.parse_general:
msg = "doesn't parse actions"
else:
msg = "must override this method"
raise NotImplementedError("derived class '%s' %s" % \
(self.__class__.__name__, msg))
def parse_args(self, args, **kwargs):
"""Parse arguments
Convert argument variables to objects and assign them as
attributes of the namespace.
Keyword arguments:
- args -- Arguments string or dict (TODO)
Returns:
The populated namespace
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)

View file

@ -1,160 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re
import argparse
import os.path
from bottle import Bottle, request, response, HTTPResponse from bottle import Bottle, request, response, HTTPResponse
from beaker.middleware import SessionMiddleware from beaker.middleware import SessionMiddleware
from . import BaseParser from ..core import MoulinetteError
from .. import MoulinetteError
from ..helpers import YunoHostError, YunoHostLDAP from ..helpers import YunoHostError, YunoHostLDAP
## API arguments Parser # API moulinette interface ---------------------------------------------
class _HTTPArgumentParser(object):
"""Argument parser for HTTP requests
Object for parsing HTTP requests into Python objects. It is based
on argparse.ArgumentParser class and implements some of its methods.
"""
def __init__(self):
# Initialize the ArgumentParser object
self._parser = argparse.ArgumentParser(usage='',
prefix_chars='@',
add_help=False)
self._parser.error = self._error
self._positional = [] # list(arg_name)
self._optional = {} # dict({arg_name: option_strings})
def set_defaults(self, **kwargs):
return self._parser.set_defaults(**kwargs)
def get_default(self, dest):
return self._parser.get_default(dest)
def add_argument(self, *args, **kwargs):
action = self._parser.add_argument(*args, **kwargs)
# Append newly created action
if len(action.option_strings) == 0:
self._positional.append(action.dest)
else:
self._optional[action.dest] = action.option_strings
return action
def parse_args(self, args):
arg_strings = []
## Append an argument to the current one
def append(arg_strings, value, option_string=None):
# TODO: Process list arguments
if isinstance(value, bool):
# Append the option string only
if option_string is not None:
arg_strings.append(option_string)
elif isinstance(value, str):
if option_string is not None:
arg_strings.append(option_string)
arg_strings.append(value)
else:
arg_strings.append(value)
return arg_strings
# Iterate over positional arguments
for dest in self._positional:
if dest in args:
arg_strings = append(arg_strings, args[dest])
# Iterate over optional arguments
for dest, opt in self._optional.items():
if dest in args:
arg_strings = append(arg_strings, args[dest], opt[0])
return self._parser.parse_args(arg_strings)
def _error(self, message):
# TODO: Raise a proper exception
raise MoulinetteError(1, message)
class APIParser(BaseParser):
"""Actions map's API Parser
"""
parse_category = False
parse_general = False
def __init__(self):
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
@property
def routes(self):
"""Get current routes"""
return self._parsers.keys()
## Implement virtual methods
@staticmethod
def format_arg_name(name, full):
if name[0] != '-':
return [name]
if full:
return [full.replace('--', '@', 1)]
if name.startswith('--'):
return [name.replace('--', '@', 1)]
return [name.replace('-', '@', 1)]
def add_action_parser(self, name, api=None, **kwargs):
"""Add a parser for an action
Keyword arguments:
- api -- The action route (e.g. 'GET /' )
Returns:
A new _HTTPArgumentParser object for the route
"""
if not api:
return None
# Validate action route
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
if not m:
return None
# Check if a parser already exists for the route
key = (m.group(1), m.group(2))
if key in self.routes:
raise ValueError("A parser for '%s' already exists" % key)
# Create and append parser
parser = _HTTPArgumentParser()
self._parsers[key] = parser
# Return the created parser
return parser
def parse_args(self, args, route, **kwargs):
"""Parse arguments
Keyword arguments:
- route -- The action route (e.g. 'GET /' )
"""
# Retrieve the parser for the route
if route not in self.routes:
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
return self._parsers[route].parse_args(args)
actionsmap_parser = APIParser
## API moulinette interface
class _APIAuthPlugin(object): class _APIAuthPlugin(object):
""" """
@ -189,7 +41,7 @@ class _APIAuthPlugin(object):
session_opts = { session_opts = {
'session.type': 'file', 'session.type': 'file',
'session.cookie_expires': True, 'session.cookie_expires': True,
'session.data_dir': pkg.cachedir('session', make_dir=True), 'session.data_dir': pkg.get_cachedir('session'),
'session.secure': True 'session.secure': True
} }
self._app = SessionMiddleware(app, session_opts) self._app = SessionMiddleware(app, session_opts)
@ -361,11 +213,11 @@ class MoulinetteAPI(object):
""" """
if category is None: if category is None:
with open(pkg.datafile('doc/resources.json')) as f: with open('%s/doc/resources.json' % pkg.datadir) as f:
return f.read() return f.read()
try: try:
with open(pkg.datafile('doc/%s.json' % category)) as f: with open('%s/doc/%s.json' % (pkg.datadir, category)) as f:
return f.read() return f.read()
except IOError: except IOError:
return 'unknown' return 'unknown'

View file

@ -1,35 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import argparse class MoulinetteCLI(object):
from . import BaseParser # TODO: Implement this class
pass
## CLI arguments Parser
class CLIParser(BaseParser):
"""Actions map's CLI Parser
"""
def __init__(self, parser=None):
self._parser = parser or argparse.ArgumentParser()
self._subparsers = self._parser.add_subparsers()
@staticmethod
def format_arg_name(name, full):
if name[0] == '-' and full:
return [name, full]
return [name]
def add_general_parser(self, **kwargs):
return self._parser
def add_category_parser(self, name, category_help=None, **kwargs):
parser = self._subparsers.add_parser(name, help=category_help)
return CLIParser(parser)
def add_action_parser(self, name, action_help, **kwargs):
return self._subparsers.add_parser(name, help=action_help)
def parse_args(self, args, **kwargs):
return self._parser.parse_args(args)
actionsmap_parser = CLIParser

15
src/moulinette/package.py Normal file
View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Public constants defined during build
"""Package's data directory (e.g. /usr/share/moulinette)"""
datadir = '/usr/share/moulinette'
"""Package's library directory (e.g. /usr/lib/moulinette)"""
libdir = '/usr/lib/moulinette'
"""Locale directory for the package (e.g. /usr/lib/moulinette/locale)"""
localedir = '/usr/lib/moulinette/locale'
"""Cache directory for the package (e.g. /var/cache/moulinette)"""
cachedir = '/var/cache/moulinette'

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Public constants defined during build
"""Package's data directory (e.g. /usr/share/moulinette)"""
datadir = %PKGDATADIR%
"""Package's library directory (e.g. /usr/lib/moulinette)"""
libdir = %PKGLIBDIR%
"""Locale directory for the package (e.g. /usr/lib/moulinette/locale)"""
localedir = %PKGLOCALEDIR%
"""Cache directory for the package (e.g. /var/cache/moulinette)"""
cachedir = %PKGCACHEDIR%