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

View file

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

View file

View file

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

View file

@ -1,21 +1,478 @@
# -*- coding: utf-8 -*-
import pickle
import argparse
import yaml
import re
import os
import cPickle as pickle
from collections import OrderedDict
import logging
from . import __version__, curr_namespace, MoulinetteError
from .extra.parameters import extraparameters_list
from . import __version__
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):
@ -78,69 +535,64 @@ class ExtraParser(object):
return arg_value
## Main class
## Main class ----------------------------------------------------------
class ActionsMap(object):
"""
Validate and process action defined into the actions map.
"""Validate and process actions defined into an 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
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.
Possible value is one of:
Keyword arguments:
- interface -- The type of interface which needs the actions map.
Possible values are:
- 'cli' for the command line interface
- '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
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
try:
# Retrieve the interface parser
mod = __import__('interface.%s' % interface,
globals=globals(), level=1,
fromlist=['actionsmap_parser'])
parser = getattr(mod, 'actionsmap_parser')
except (AttributeError, ImportError):
self._parser_class = actionsmap_parsers[interface]
except KeyError:
raise MoulinetteError(22, _("Invalid interface '%s'" % interface))
else:
self._parser_class = parser
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
if len(namespaces) == 0:
namespaces = self.get_namespaces()
actionsmaps = {}
namespaces = self.get_actionsmap_namespaces()
if curr_namespace and curr_namespace in namespaces:
namespaces = [curr_namespace]
# Iterate over actions map namespaces
for n in namespaces:
logging.debug("loading '%s' actions map namespace" % n)
if use_cache:
# Attempt to load cache if it exists
cache_file = '%s/%s.pkl' % (pkg.cachedir('actionsmap'), n)
if os.path.isfile(cache_file):
with open(cache_file, 'r') as f:
try:
# Attempt to load cache
with open('%s/actionsmap/%s.pkl' % (pkg.cachedir, n)) as f:
actionsmaps[n] = pickle.load(f)
else:
# TODO: Switch to python3 and catch proper exception
except IOError:
self.use_cache = False
actionsmaps = self.generate_cache(namespaces)
break
else:
am_file = '%s/%s.yml' % (pkg.datadir('actionsmap'), n)
with open(am_file, 'r') as f:
with open('%s/actionsmap/%s.yml' % (pkg.datadir, n)) as f:
actionsmaps[n] = yaml.load(f)
# Generate parsers
self.extraparser = ExtraParser(interface)
self.extraparser = ExtraArgumentParser(interface)
self.parser = self._construct_parser(actionsmaps)
def process(self, args, **kwargs):
@ -152,6 +604,8 @@ class ActionsMap(object):
- **kwargs -- Additional interface arguments
"""
# Check moulinette status
# Parse arguments
arguments = vars(self.parser.parse_args(args, **kwargs))
arguments = self._parse_extra_parameters(arguments)
@ -170,11 +624,12 @@ class ActionsMap(object):
else:
# Process the action
return func(**arguments)
return {}
@staticmethod
def get_actionsmap_namespaces():
def get_namespaces():
"""
Retrieve actions map namespaces from a given path
Retrieve available actions map namespaces
Returns:
A list of available namespaces
@ -182,7 +637,7 @@ class ActionsMap(object):
"""
namespaces = []
for f in os.listdir(pkg.datadir('actionsmap')):
for f in os.listdir('%s/actionsmap' % pkg.datadir):
if f.endswith('.yml'):
namespaces.append(f[:-4])
return namespaces
@ -201,20 +656,19 @@ class ActionsMap(object):
"""
actionsmaps = {}
if not namespaces:
namespaces = klass.get_actionsmap_namespaces()
namespaces = klass.get_namespaces()
# Iterate over actions map namespaces
for n in namespaces:
logging.debug("generating cache for '%s' actions map namespace" % n)
# 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:
actionsmaps[n] = yaml.load(f)
# Cache actions map into pickle file
cache_file = pkg.cachefile('actionsmap/%s.pkl' % n, make_dir=True)
with open(cache_file, 'w') as f:
with pkg.open_cache('%s.pkl' % n, subdir='actionsmap') as f:
pickle.dump(actionsmaps[n], f)
return actionsmaps
@ -291,41 +745,41 @@ class ActionsMap(object):
for n, actionsmap in actionsmaps.items():
if 'general_arguments' in actionsmap:
# Parse general arguments
if top_parser.parse_general:
if top_parser.parse_general_arguments:
parser = top_parser.add_general_parser()
for an, ap in actionsmap['general_arguments'].items():
if 'version' in ap:
ap['version'] = ap['version'].replace('%version%',
__version__)
# Replace version number
version = ap.get('version', None)
if version:
ap['version'] = version.replace('%version%',
__version__)
argname = top_parser.format_arg_name(an, ap.pop('full', None))
parser.add_argument(*argname, **ap)
del actionsmap['general_arguments']
# Parse categories
for cn, cp in actionsmap.items():
if 'actions' not in cp:
try:
actions = cp.pop('actions')
except KeyError:
continue
actions = cp.pop('actions')
# Add category parser
if top_parser.parse_category:
cat_parser = top_parser.add_category_parser(cn, **cp)
else:
cat_parser = top_parser
cat_parser = top_parser.add_category_parser(cn, **cp)
# Parse actions
if not top_parser.parse_action:
continue
for an, ap in actions.items():
arguments = ap.pop('arguments', {})
# Add action parser
parser = cat_parser.add_action_parser(an, **ap)
if not parser:
continue
# Store action information
parser.set_defaults(_info=(n, cn, an))
try:
# Store action information
parser.set_defaults(_info=(n, cn, an))
except AttributeError:
# No parser for the action
break
# Add action arguments
for argn, argp in arguments.items():

View file

@ -5,115 +5,126 @@ import sys
import gettext
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):
"""Package representation and easy access
"""Package representation and easy access methods
Initialize directories and variables for the package and give them
easy access.
Keyword arguments:
- prefix -- The installation prefix
- libdir -- The library directory; usually, this would be
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.
- _from_source -- Either the package is running from source or
not (only for debugging)
"""
def __init__(self, prefix, libdir, cachedir, destdir=None):
if not prefix and not libdir:
# Running from source directory
def __init__(self, _from_source=False):
if _from_source:
import sys
basedir = os.path.abspath(os.path.dirname(sys.argv[0]) +'/../')
self._datadir = os.path.join(basedir, 'data')
self._libdir = os.path.join(basedir, 'src')
self._cachedir = cachedir
# Set local directories
self._datadir = '%s/data' % basedir
self._libdir = '%s/lib' % basedir
self._localedir = '%s/po' % basedir
self._cachedir = '%s/cache' % basedir
else:
self._datadir = os.path.join(prefix, 'share/moulinette')
self._libdir = os.path.join(libdir, 'moulinette')
self._cachedir = os.path.join(cachedir, 'moulinette')
import package
# Append library path to python's path
sys.path.append(self._libdir)
self._destdir = destdir or None
# Set system directories
self._datadir = package.datadir
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):
"""Return the path to a data directory"""
return self.get_dir(self._datadir, subdir, **kwargs)
@property
def datadir(self):
"""Return the data directory of the package"""
return self._datadir
def datafile(self, filename, **kwargs):
"""Return the path to a data file"""
return self.get_file(self._datadir, filename, **kwargs)
@property
def libdir(self):
"""Return the lib directory of the package"""
return self._libdir
def libdir(self, subdir=None, **kwargs):
"""Return the path to a lib directory"""
return self.get_dir(self._libdir, subdir, **kwargs)
@property
def localedir(self):
"""Return the locale directory of the package"""
return self._localedir
def libfile(self, filename, **kwargs):
"""Return the path to a lib file"""
return self.get_file(self._libdir, filename, **kwargs)
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)
@property
def cachedir(self):
"""Return the cache directory of the package"""
return self._cachedir
## Standard methods
## Additional methods
def get_dir(self, basedir, subdir=None, make_dir=False):
"""Get a directory path
def get_cachedir(self, subdir='', make_dir=True):
"""Get the path to a cache directory
Return a path composed by a base directory and an optional
subdirectory. The path will be created if needed.
Return the path to the cache directory from an optional
subdirectory and create it if needed.
Keyword arguments:
- basedir -- The base directory
- subdir -- An optional subdirectory
- make_dir -- True if it should create needed directory
- subdir -- A cache subdirectory
- make_dir -- False to not make directory if it not exists
"""
# Retrieve path
path = basedir
if self._destdir:
path = os.path.join(self._destdir, path)
if subdir:
path = os.path.join(path, subdir)
path = os.path.join(self.cachedir, subdir)
# Create directory
if make_dir and not os.path.isdir(path):
os.makedirs(path)
return path
def get_file(self, basedir, filename, **kwargs):
"""Get a file path
def open_cache(self, filename, subdir='', mode='w'):
"""Open a cache file and return a stream
Return the path of the filename in the specified directory. This
directory will be created if needed.
Attempt to open in 'mode' the cache file 'filename' from the
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:
- basedir -- The base directory of the file
- filename -- The filename or a path relative to basedir
- **kwargs -- Additional arguments for Package.get_dir
- filename -- The cache filename
- subdir -- A subdirectory which contains the file
- mode -- The mode in which the file is opened
"""
# Check for a directory in filename
subdir = os.path.dirname(filename) or None
if subdir:
filename = os.path.basename(filename)
return open('%s/%s' % (self.get_cachedir(subdir), filename), mode)
# Get directory path
dirpath = self.get_dir(basedir, subdir, **kwargs)
return os.path.join(dirpath, filename)
# Moulinette core classes ----------------------------------------------
class MoulinetteError(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 -*-
import os
import sys
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
import ldap.modlist as modlist
import yaml
import json
import re
import getpass
import random
import string
import argparse
import gettext
import getpass
if not __debug__:
import traceback
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 -*-
import re
import argparse
import os.path
from bottle import Bottle, request, response, HTTPResponse
from beaker.middleware import SessionMiddleware
from . import BaseParser
from .. import MoulinetteError
from ..core import MoulinetteError
from ..helpers import YunoHostError, YunoHostLDAP
## API arguments 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 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
# API moulinette interface ---------------------------------------------
class _APIAuthPlugin(object):
"""
@ -189,7 +41,7 @@ class _APIAuthPlugin(object):
session_opts = {
'session.type': 'file',
'session.cookie_expires': True,
'session.data_dir': pkg.cachedir('session', make_dir=True),
'session.data_dir': pkg.get_cachedir('session'),
'session.secure': True
}
self._app = SessionMiddleware(app, session_opts)
@ -361,11 +213,11 @@ class MoulinetteAPI(object):
"""
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()
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()
except IOError:
return 'unknown'

View file

@ -1,35 +1,5 @@
# -*- coding: utf-8 -*-
import argparse
from . import BaseParser
## 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
class MoulinetteCLI(object):
# TODO: Implement this class
pass

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%