mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
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:
parent
9104024fa1
commit
9c9ccc1271
27 changed files with 660 additions and 632 deletions
16
bin/yunohost
16
bin/yunohost
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
0
src/moulinette/extra/__init__.py → lib/yunohost/__init__.py
Normal file → Executable file
0
src/moulinette/extra/__init__.py → lib/yunohost/__init__.py
Normal file → Executable 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"))
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
|
@ -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
114
src/moulinette/interface/__init__.py
Executable file → Normal 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__)
|
|
@ -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'
|
||||
|
|
|
@ -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
15
src/moulinette/package.py
Normal 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'
|
15
src/moulinette/package.py.in
Normal file
15
src/moulinette/package.py.in
Normal 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%
|
Loading…
Reference in a new issue