mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Rework actionsmap and m18n init, drop multiple actionsmap support
This commit is contained in:
parent
b2c67369a8
commit
9fcc9630bd
10 changed files with 185 additions and 303 deletions
|
@ -3,7 +3,6 @@
|
||||||
from moulinette.core import (
|
from moulinette.core import (
|
||||||
MoulinetteError,
|
MoulinetteError,
|
||||||
Moulinette18n,
|
Moulinette18n,
|
||||||
env,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__title__ = "moulinette"
|
__title__ = "moulinette"
|
||||||
|
@ -54,35 +53,8 @@ class Moulinette:
|
||||||
return cls._interface
|
return cls._interface
|
||||||
|
|
||||||
|
|
||||||
# Package functions
|
|
||||||
|
|
||||||
|
|
||||||
def init(logging_config=None, **kwargs):
|
|
||||||
"""Package initialization
|
|
||||||
|
|
||||||
Initialize directories and global variables. It must be called
|
|
||||||
before any of package method is used - even the easy access
|
|
||||||
functions.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- logging_config -- A dict containing logging configuration to load
|
|
||||||
- **kwargs -- See core.Package
|
|
||||||
|
|
||||||
At the end, the global variable 'pkg' will contain a Package
|
|
||||||
instance. See core.Package for available methods and variables.
|
|
||||||
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
from moulinette.utils.log import configure_logging
|
|
||||||
|
|
||||||
configure_logging(logging_config)
|
|
||||||
|
|
||||||
# Add library directory to python path
|
|
||||||
sys.path.insert(0, env["LIB_DIR"])
|
|
||||||
|
|
||||||
|
|
||||||
# Easy access to interfaces
|
# Easy access to interfaces
|
||||||
def api(host="localhost", port=80, routes={}):
|
def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None):
|
||||||
"""Web server (API) interface
|
"""Web server (API) interface
|
||||||
|
|
||||||
Run a HTTP server with the moulinette for an API usage.
|
Run a HTTP server with the moulinette for an API usage.
|
||||||
|
@ -96,8 +68,16 @@ def api(host="localhost", port=80, routes={}):
|
||||||
"""
|
"""
|
||||||
from moulinette.interfaces.api import Interface as Api
|
from moulinette.interfaces.api import Interface as Api
|
||||||
|
|
||||||
|
m18n.set_locales_dir(locales_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Api(routes=routes).run(host, port)
|
Api(
|
||||||
|
routes=routes,
|
||||||
|
actionsmap=actionsmap,
|
||||||
|
).run(
|
||||||
|
host,
|
||||||
|
port
|
||||||
|
)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -110,7 +90,7 @@ def api(host="localhost", port=80, routes={}):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cli(args, top_parser, output_as=None, timeout=None):
|
def cli(args, top_parser, output_as=None, timeout=None, actionsmap=None, locales_dir=None):
|
||||||
"""Command line interface
|
"""Command line interface
|
||||||
|
|
||||||
Execute an action with the moulinette from the CLI and print its
|
Execute an action with the moulinette from the CLI and print its
|
||||||
|
@ -125,10 +105,18 @@ def cli(args, top_parser, output_as=None, timeout=None):
|
||||||
"""
|
"""
|
||||||
from moulinette.interfaces.cli import Interface as Cli
|
from moulinette.interfaces.cli import Interface as Cli
|
||||||
|
|
||||||
|
m18n.set_locales_dir(locales_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
load_only_category = args[0] if args and not args[0].startswith("-") else None
|
load_only_category = args[0] if args and not args[0].startswith("-") else None
|
||||||
Cli(top_parser=top_parser, load_only_category=load_only_category).run(
|
Cli(
|
||||||
args, output_as=output_as, timeout=timeout
|
top_parser=top_parser,
|
||||||
|
load_only_category=load_only_category,
|
||||||
|
actionsmap=actionsmap,
|
||||||
|
).run(
|
||||||
|
args,
|
||||||
|
output_as=output_as,
|
||||||
|
timeout=timeout
|
||||||
)
|
)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -16,7 +16,6 @@ from moulinette.core import (
|
||||||
MoulinetteError,
|
MoulinetteError,
|
||||||
MoulinetteLock,
|
MoulinetteLock,
|
||||||
MoulinetteValidationError,
|
MoulinetteValidationError,
|
||||||
env,
|
|
||||||
)
|
)
|
||||||
from moulinette.interfaces import BaseActionsMapParser, TO_RETURN_PROP
|
from moulinette.interfaces import BaseActionsMapParser, TO_RETURN_PROP
|
||||||
from moulinette.utils.log import start_action_logging
|
from moulinette.utils.log import start_action_logging
|
||||||
|
@ -382,9 +381,6 @@ class ActionsMap(object):
|
||||||
It is composed by categories which contain one or more action(s).
|
It is composed by categories which contain one or more action(s).
|
||||||
Moreover, the action can have specific argument(s).
|
Moreover, the action can have specific argument(s).
|
||||||
|
|
||||||
This class allows to manipulate one or several actions maps
|
|
||||||
associated to a namespace.
|
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
||||||
parsing the actions map
|
parsing the actions map
|
||||||
|
@ -394,84 +390,71 @@ class ActionsMap(object):
|
||||||
purposes...
|
purposes...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, top_parser, load_only_category=None):
|
def __init__(self, actionsmap_yml, top_parser, load_only_category=None):
|
||||||
|
|
||||||
assert isinstance(top_parser, BaseActionsMapParser), (
|
assert isinstance(top_parser, BaseActionsMapParser), (
|
||||||
"Invalid parser class '%s'" % top_parser.__class__.__name__
|
"Invalid parser class '%s'" % top_parser.__class__.__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
DATA_DIR = env["DATA_DIR"]
|
|
||||||
CACHE_DIR = env["CACHE_DIR"]
|
|
||||||
|
|
||||||
actionsmaps = OrderedDict()
|
|
||||||
|
|
||||||
self.from_cache = False
|
self.from_cache = False
|
||||||
# Iterate over actions map namespaces
|
|
||||||
for n in self.get_namespaces():
|
|
||||||
logger.debug("loading actions map namespace '%s'", n)
|
|
||||||
|
|
||||||
actionsmap_yml = "%s/actionsmap/%s.yml" % (DATA_DIR, n)
|
logger.debug("loading actions map")
|
||||||
actionsmap_yml_stat = os.stat(actionsmap_yml)
|
|
||||||
actionsmap_pkl = "%s/actionsmap/%s-%d-%d.pkl" % (
|
|
||||||
CACHE_DIR,
|
|
||||||
n,
|
|
||||||
actionsmap_yml_stat.st_size,
|
|
||||||
actionsmap_yml_stat.st_mtime,
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_cache():
|
actionsmap_yml_dir = os.path.dirname(actionsmap_yml)
|
||||||
|
actionsmap_yml_file = os.path.basename(actionsmap_yml)
|
||||||
|
actionsmap_yml_stat = os.stat(actionsmap_yml)
|
||||||
|
|
||||||
# Iterate over actions map namespaces
|
actionsmap_pkl = f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.{actionsmap_yml_stat.st_size}-{actionsmap_yml_stat.st_mtime}.pkl"
|
||||||
logger.debug("generating cache for actions map namespace '%s'", n)
|
|
||||||
|
|
||||||
# Read actions map from yaml file
|
def generate_cache():
|
||||||
actionsmap = read_yaml(actionsmap_yml)
|
|
||||||
|
|
||||||
# Delete old cache files
|
logger.debug("generating cache for actions map")
|
||||||
for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, n)):
|
|
||||||
os.remove(old_cache)
|
|
||||||
|
|
||||||
# at installation, cachedir might not exists
|
# Read actions map from yaml file
|
||||||
dir_ = os.path.dirname(actionsmap_pkl)
|
actionsmap = read_yaml(actionsmap_yml)
|
||||||
if not os.path.isdir(dir_):
|
|
||||||
os.makedirs(dir_)
|
|
||||||
|
|
||||||
# Cache actions map into pickle file
|
# Delete old cache files
|
||||||
with open(actionsmap_pkl, "wb") as f:
|
for old_cache in glob.glob(f"{actionsmap_yml_dir}/.{actionsmap_yml_file}.*.pkl"):
|
||||||
pickle.dump(actionsmap, f)
|
os.remove(old_cache)
|
||||||
|
|
||||||
return actionsmap
|
# at installation, cachedir might not exists
|
||||||
|
dir_ = os.path.dirname(actionsmap_pkl)
|
||||||
|
if not os.path.isdir(dir_):
|
||||||
|
os.makedirs(dir_)
|
||||||
|
|
||||||
if os.path.exists(actionsmap_pkl):
|
# Cache actions map into pickle file
|
||||||
try:
|
with open(actionsmap_pkl, "wb") as f:
|
||||||
# Attempt to load cache
|
pickle.dump(actionsmap, f)
|
||||||
with open(actionsmap_pkl, "rb") as f:
|
|
||||||
actionsmaps[n] = pickle.load(f)
|
|
||||||
|
|
||||||
self.from_cache = True
|
return actionsmap
|
||||||
# TODO: Switch to python3 and catch proper exception
|
|
||||||
except (IOError, EOFError):
|
|
||||||
actionsmaps[n] = generate_cache()
|
|
||||||
else: # cache file doesn't exists
|
|
||||||
actionsmaps[n] = generate_cache()
|
|
||||||
|
|
||||||
# If load_only_category is set, and *if* the target category
|
if os.path.exists(actionsmap_pkl):
|
||||||
# is in the actionsmap, we'll load only that one.
|
try:
|
||||||
# If we filter it even if it doesn't exist, we'll end up with a
|
# Attempt to load cache
|
||||||
# weird help message when we do a typo in the category name..
|
with open(actionsmap_pkl, "rb") as f:
|
||||||
if load_only_category and load_only_category in actionsmaps[n]:
|
actionsmap = pickle.load(f)
|
||||||
actionsmaps[n] = {
|
|
||||||
k: v
|
|
||||||
for k, v in actionsmaps[n].items()
|
|
||||||
if k in [load_only_category, "_global"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load translations
|
self.from_cache = True
|
||||||
m18n.load_namespace(n)
|
# TODO: Switch to python3 and catch proper exception
|
||||||
|
except (IOError, EOFError):
|
||||||
|
actionsmap = generate_cache()
|
||||||
|
else: # cache file doesn't exists
|
||||||
|
actionsmap = generate_cache()
|
||||||
|
|
||||||
|
# If load_only_category is set, and *if* the target category
|
||||||
|
# is in the actionsmap, we'll load only that one.
|
||||||
|
# If we filter it even if it doesn't exist, we'll end up with a
|
||||||
|
# weird help message when we do a typo in the category name..
|
||||||
|
if load_only_category and load_only_category in actionsmap:
|
||||||
|
actionsmap = {
|
||||||
|
k: v
|
||||||
|
for k, v in actionsmap.items()
|
||||||
|
if k in [load_only_category, "_global"]
|
||||||
|
}
|
||||||
|
|
||||||
# Generate parsers
|
# Generate parsers
|
||||||
self.extraparser = ExtraArgumentParser(top_parser.interface)
|
self.extraparser = ExtraArgumentParser(top_parser.interface)
|
||||||
self.parser = self._construct_parser(actionsmaps, top_parser)
|
self.parser = self._construct_parser(actionsmap, top_parser)
|
||||||
|
|
||||||
def get_authenticator(self, auth_method):
|
def get_authenticator(self, auth_method):
|
||||||
|
|
||||||
|
@ -479,7 +462,7 @@ class ActionsMap(object):
|
||||||
auth_method = self.default_authentication
|
auth_method = self.default_authentication
|
||||||
|
|
||||||
# Load and initialize the authenticator module
|
# Load and initialize the authenticator module
|
||||||
auth_module = "%s.authenticators.%s" % (self.main_namespace, auth_method)
|
auth_module = "%s.authenticators.%s" % (self.namespace, auth_method)
|
||||||
logger.debug(f"Loading auth module {auth_module}")
|
logger.debug(f"Loading auth module {auth_module}")
|
||||||
try:
|
try:
|
||||||
mod = import_module(auth_module)
|
mod = import_module(auth_module)
|
||||||
|
@ -591,7 +574,6 @@ class ActionsMap(object):
|
||||||
logger.debug("processing action [%s]: %s", log_id, full_action_name)
|
logger.debug("processing action [%s]: %s", log_id, full_action_name)
|
||||||
|
|
||||||
# Load translation and process the action
|
# Load translation and process the action
|
||||||
m18n.load_namespace(namespace)
|
|
||||||
start = time()
|
start = time()
|
||||||
try:
|
try:
|
||||||
return func(**arguments)
|
return func(**arguments)
|
||||||
|
@ -599,43 +581,14 @@ class ActionsMap(object):
|
||||||
stop = time()
|
stop = time()
|
||||||
logger.debug("action [%s] executed in %.3fs", log_id, stop - start)
|
logger.debug("action [%s] executed in %.3fs", log_id, stop - start)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_namespaces():
|
|
||||||
"""
|
|
||||||
Retrieve available actions map namespaces
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of available namespaces
|
|
||||||
|
|
||||||
"""
|
|
||||||
namespaces = []
|
|
||||||
|
|
||||||
DATA_DIR = env["DATA_DIR"]
|
|
||||||
|
|
||||||
# This var is ['*'] by default but could be set for example to
|
|
||||||
# ['yunohost', 'yml_*']
|
|
||||||
NAMESPACE_PATTERNS = env["NAMESPACES"].split()
|
|
||||||
|
|
||||||
# Look for all files that match the given patterns in the actionsmap dir
|
|
||||||
for namespace_pattern in NAMESPACE_PATTERNS:
|
|
||||||
namespaces.extend(
|
|
||||||
glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep only the filenames with extension
|
|
||||||
namespaces = [os.path.basename(n)[:-4] for n in namespaces]
|
|
||||||
|
|
||||||
return namespaces
|
|
||||||
|
|
||||||
# Private methods
|
# Private methods
|
||||||
|
|
||||||
def _construct_parser(self, actionsmaps, top_parser):
|
def _construct_parser(self, actionsmap, top_parser):
|
||||||
"""
|
"""
|
||||||
Construct the parser with the actions map
|
Construct the parser with the actions map
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- actionsmaps -- A dict of multi-level dictionnary of
|
- actionsmap -- A dictionnary of categories/actions/arguments list
|
||||||
categories/actions/arguments list for each namespaces
|
|
||||||
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
||||||
parsing the actions map
|
parsing the actions map
|
||||||
|
|
||||||
|
@ -658,52 +611,85 @@ class ActionsMap(object):
|
||||||
# * namespace define the top "name", for us it will always be
|
# * namespace define the top "name", for us it will always be
|
||||||
# "yunohost" and there well be only this one
|
# "yunohost" and there well be only this one
|
||||||
# * actionsmap is the actual actionsmap that we care about
|
# * actionsmap is the actual actionsmap that we care about
|
||||||
for namespace, actionsmap in actionsmaps.items():
|
|
||||||
# Retrieve global parameters
|
|
||||||
_global = actionsmap.pop("_global", {})
|
|
||||||
|
|
||||||
if _global:
|
# Retrieve global parameters
|
||||||
if getattr(self, "main_namespace", None) is not None:
|
_global = actionsmap.pop("_global", {})
|
||||||
raise MoulinetteError(
|
|
||||||
"It is not possible to have several namespaces with a _global section"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.main_namespace = namespace
|
|
||||||
self.name = _global["name"]
|
|
||||||
self.default_authentication = _global["authentication"][
|
|
||||||
interface_type
|
|
||||||
]
|
|
||||||
|
|
||||||
if top_parser.has_global_parser():
|
self.namespace = _global["namespace"]
|
||||||
top_parser.add_global_arguments(_global["arguments"])
|
self.cookie_name = _global["cookie_name"]
|
||||||
|
self.default_authentication = _global["authentication"][
|
||||||
|
interface_type
|
||||||
|
]
|
||||||
|
|
||||||
if not hasattr(self, "main_namespace"):
|
if top_parser.has_global_parser():
|
||||||
raise MoulinetteError("Did not found the main namespace", raw_msg=True)
|
top_parser.add_global_arguments(_global["arguments"])
|
||||||
|
|
||||||
for namespace, actionsmap in actionsmaps.items():
|
# category_name is stuff like "user", "domain", "hooks"...
|
||||||
# category_name is stuff like "user", "domain", "hooks"...
|
# category_values is the values of this category (like actions)
|
||||||
# category_values is the values of this category (like actions)
|
for category_name, category_values in actionsmap.items():
|
||||||
for category_name, category_values in actionsmap.items():
|
|
||||||
|
|
||||||
actions = category_values.pop("actions", {})
|
actions = category_values.pop("actions", {})
|
||||||
subcategories = category_values.pop("subcategories", {})
|
subcategories = category_values.pop("subcategories", {})
|
||||||
|
|
||||||
# Get category parser
|
# Get category parser
|
||||||
category_parser = top_parser.add_category_parser(
|
category_parser = top_parser.add_category_parser(
|
||||||
category_name, **category_values
|
category_name, **category_values
|
||||||
|
)
|
||||||
|
|
||||||
|
# action_name is like "list" of "domain list"
|
||||||
|
# action_options are the values
|
||||||
|
for action_name, action_options in actions.items():
|
||||||
|
arguments = action_options.pop("arguments", {})
|
||||||
|
authentication = action_options.pop("authentication", {})
|
||||||
|
tid = (self.namespace, category_name, action_name)
|
||||||
|
|
||||||
|
# Get action parser
|
||||||
|
action_parser = category_parser.add_action_parser(
|
||||||
|
action_name, tid, **action_options
|
||||||
)
|
)
|
||||||
|
|
||||||
# action_name is like "list" of "domain list"
|
if action_parser is None: # No parser for the action
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store action identifier and add arguments
|
||||||
|
action_parser.set_defaults(_tid=tid)
|
||||||
|
action_parser.add_arguments(
|
||||||
|
arguments,
|
||||||
|
extraparser=self.extraparser,
|
||||||
|
format_arg_names=top_parser.format_arg_names,
|
||||||
|
validate_extra=validate_extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
action_parser.authentication = self.default_authentication
|
||||||
|
if interface_type in authentication:
|
||||||
|
action_parser.authentication = authentication[interface_type]
|
||||||
|
|
||||||
|
# subcategory_name is like "cert" in "domain cert status"
|
||||||
|
# subcategory_values is the values of this subcategory (like actions)
|
||||||
|
for subcategory_name, subcategory_values in subcategories.items():
|
||||||
|
|
||||||
|
actions = subcategory_values.pop("actions")
|
||||||
|
|
||||||
|
# Get subcategory parser
|
||||||
|
subcategory_parser = category_parser.add_subcategory_parser(
|
||||||
|
subcategory_name, **subcategory_values
|
||||||
|
)
|
||||||
|
|
||||||
|
# action_name is like "status" of "domain cert status"
|
||||||
# action_options are the values
|
# action_options are the values
|
||||||
for action_name, action_options in actions.items():
|
for action_name, action_options in actions.items():
|
||||||
arguments = action_options.pop("arguments", {})
|
arguments = action_options.pop("arguments", {})
|
||||||
authentication = action_options.pop("authentication", {})
|
authentication = action_options.pop("authentication", {})
|
||||||
tid = (namespace, category_name, action_name)
|
tid = (self.namespace, category_name, subcategory_name, action_name)
|
||||||
|
|
||||||
# Get action parser
|
try:
|
||||||
action_parser = category_parser.add_action_parser(
|
# Get action parser
|
||||||
action_name, tid, **action_options
|
action_parser = subcategory_parser.add_action_parser(
|
||||||
)
|
action_name, tid, **action_options
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
# No parser for the action
|
||||||
|
continue
|
||||||
|
|
||||||
if action_parser is None: # No parser for the action
|
if action_parser is None: # No parser for the action
|
||||||
continue
|
continue
|
||||||
|
@ -719,52 +705,9 @@ class ActionsMap(object):
|
||||||
|
|
||||||
action_parser.authentication = self.default_authentication
|
action_parser.authentication = self.default_authentication
|
||||||
if interface_type in authentication:
|
if interface_type in authentication:
|
||||||
action_parser.authentication = authentication[interface_type]
|
action_parser.authentication = authentication[
|
||||||
|
interface_type
|
||||||
# subcategory_name is like "cert" in "domain cert status"
|
]
|
||||||
# subcategory_values is the values of this subcategory (like actions)
|
|
||||||
for subcategory_name, subcategory_values in subcategories.items():
|
|
||||||
|
|
||||||
actions = subcategory_values.pop("actions")
|
|
||||||
|
|
||||||
# Get subcategory parser
|
|
||||||
subcategory_parser = category_parser.add_subcategory_parser(
|
|
||||||
subcategory_name, **subcategory_values
|
|
||||||
)
|
|
||||||
|
|
||||||
# action_name is like "status" of "domain cert status"
|
|
||||||
# action_options are the values
|
|
||||||
for action_name, action_options in actions.items():
|
|
||||||
arguments = action_options.pop("arguments", {})
|
|
||||||
authentication = action_options.pop("authentication", {})
|
|
||||||
tid = (namespace, category_name, subcategory_name, action_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get action parser
|
|
||||||
action_parser = subcategory_parser.add_action_parser(
|
|
||||||
action_name, tid, **action_options
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
# No parser for the action
|
|
||||||
continue
|
|
||||||
|
|
||||||
if action_parser is None: # No parser for the action
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store action identifier and add arguments
|
|
||||||
action_parser.set_defaults(_tid=tid)
|
|
||||||
action_parser.add_arguments(
|
|
||||||
arguments,
|
|
||||||
extraparser=self.extraparser,
|
|
||||||
format_arg_names=top_parser.format_arg_names,
|
|
||||||
validate_extra=validate_extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
action_parser.authentication = self.default_authentication
|
|
||||||
if interface_type in authentication:
|
|
||||||
action_parser.authentication = authentication[
|
|
||||||
interface_type
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.debug("building parser took %.3fs", time() - start)
|
logger.debug("building parser took %.3fs", time() - start)
|
||||||
return top_parser
|
return top_parser
|
||||||
|
|
|
@ -9,24 +9,10 @@ import moulinette
|
||||||
|
|
||||||
logger = logging.getLogger("moulinette.core")
|
logger = logging.getLogger("moulinette.core")
|
||||||
|
|
||||||
env = {
|
|
||||||
"DATA_DIR": "/usr/share/moulinette",
|
|
||||||
"LIB_DIR": "/usr/lib/moulinette",
|
|
||||||
"LOCALES_DIR": "/usr/share/moulinette/locale",
|
|
||||||
"CACHE_DIR": "/var/cache/moulinette",
|
|
||||||
"NAMESPACES": "*", # By default we'll load every namespace we find
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in env.keys():
|
|
||||||
value_from_environ = os.environ.get(f"MOULINETTE_{key}")
|
|
||||||
if value_from_environ:
|
|
||||||
env[key] = value_from_environ
|
|
||||||
|
|
||||||
|
|
||||||
def during_unittests_run():
|
def during_unittests_run():
|
||||||
return "TESTS_RUN" in os.environ
|
return "TESTS_RUN" in os.environ
|
||||||
|
|
||||||
|
|
||||||
# Internationalization -------------------------------------------------
|
# Internationalization -------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,11 +37,7 @@ class Translator(object):
|
||||||
# Attempt to load default translations
|
# Attempt to load default translations
|
||||||
if not self._load_translations(default_locale):
|
if not self._load_translations(default_locale):
|
||||||
logger.error(
|
logger.error(
|
||||||
"unable to load locale '%s' from '%s'. Does the file '%s/%s.json' exists?",
|
f"unable to load locale '{default_locale}' from '{locale_dir}'. Does the file '{locale_dir}/{default_locale}.json' exists?",
|
||||||
default_locale,
|
|
||||||
locale_dir,
|
|
||||||
locale_dir,
|
|
||||||
default_locale,
|
|
||||||
)
|
)
|
||||||
self.default_locale = default_locale
|
self.default_locale = default_locale
|
||||||
|
|
||||||
|
@ -207,44 +189,23 @@ class Moulinette18n(object):
|
||||||
self.default_locale = default_locale
|
self.default_locale = default_locale
|
||||||
self.locale = default_locale
|
self.locale = default_locale
|
||||||
|
|
||||||
self.locales_dir = env["LOCALES_DIR"]
|
|
||||||
|
|
||||||
# Init global translator
|
# Init global translator
|
||||||
self._global = Translator(self.locales_dir, default_locale)
|
global_locale_dir = "/usr/share/moulinette/locales"
|
||||||
|
if during_unittests_run():
|
||||||
|
global_locale_dir = os.path.dirname(__file__) + "/../locales"
|
||||||
|
|
||||||
# Define namespace related variables
|
self._global = Translator(global_locale_dir, default_locale)
|
||||||
self._namespaces = {}
|
|
||||||
self._current_namespace = None
|
|
||||||
|
|
||||||
def load_namespace(self, namespace):
|
def set_locales_dir(self, locales_dir):
|
||||||
"""Load the namespace to use
|
|
||||||
|
|
||||||
Load and set translations of a given namespace. Those translations
|
self.translator = Translator(locales_dir, self.default_locale)
|
||||||
are accessible with Moulinette18n.n().
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- namespace -- The namespace to load
|
|
||||||
|
|
||||||
"""
|
|
||||||
if namespace not in self._namespaces:
|
|
||||||
# Create new Translator object
|
|
||||||
lib_dir = env["LIB_DIR"]
|
|
||||||
translator = Translator(
|
|
||||||
"%s/%s/locales" % (lib_dir, namespace), self.default_locale
|
|
||||||
)
|
|
||||||
translator.set_locale(self.locale)
|
|
||||||
self._namespaces[namespace] = translator
|
|
||||||
|
|
||||||
# Set current namespace
|
|
||||||
self._current_namespace = namespace
|
|
||||||
|
|
||||||
def set_locale(self, locale):
|
def set_locale(self, locale):
|
||||||
"""Set the locale to use"""
|
"""Set the locale to use"""
|
||||||
self.locale = locale
|
|
||||||
|
|
||||||
|
self.locale = locale
|
||||||
self._global.set_locale(locale)
|
self._global.set_locale(locale)
|
||||||
for n in self._namespaces.values():
|
self.translator.set_locale(locale)
|
||||||
n.set_locale(locale)
|
|
||||||
|
|
||||||
def g(self, key: str, *args, **kwargs) -> str:
|
def g(self, key: str, *args, **kwargs) -> str:
|
||||||
"""Retrieve proper translation for a moulinette key
|
"""Retrieve proper translation for a moulinette key
|
||||||
|
@ -269,7 +230,7 @@ class Moulinette18n(object):
|
||||||
- key -- The key to translate
|
- key -- The key to translate
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._namespaces[self._current_namespace].translate(key, *args, **kwargs)
|
return self.translator.translate(key, *args, **kwargs)
|
||||||
|
|
||||||
def key_exists(self, key: str) -> bool:
|
def key_exists(self, key: str) -> bool:
|
||||||
"""Check if a key exists in the translation files
|
"""Check if a key exists in the translation files
|
||||||
|
@ -278,7 +239,7 @@ class Moulinette18n(object):
|
||||||
- key -- The key to translate
|
- key -- The key to translate
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._namespaces[self._current_namespace].key_exists(key)
|
return self.translator.key_exists(key)
|
||||||
|
|
||||||
|
|
||||||
# Moulinette core classes ----------------------------------------------
|
# Moulinette core classes ----------------------------------------------
|
||||||
|
|
|
@ -237,14 +237,14 @@ class _HTTPArgumentParser(object):
|
||||||
class Session:
|
class Session:
|
||||||
|
|
||||||
secret = random_ascii()
|
secret = random_ascii()
|
||||||
actionsmap_name = None # This is later set to the actionsmap name
|
cookie_name = None # This is later set to the actionsmap name
|
||||||
|
|
||||||
def set_infos(infos):
|
def set_infos(infos):
|
||||||
|
|
||||||
assert isinstance(infos, dict)
|
assert isinstance(infos, dict)
|
||||||
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
f"session.{Session.actionsmap_name}",
|
f"session.{Session.cookie_name}",
|
||||||
infos,
|
infos,
|
||||||
secure=True,
|
secure=True,
|
||||||
secret=Session.secret,
|
secret=Session.secret,
|
||||||
|
@ -256,7 +256,7 @@ class Session:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
infos = request.get_cookie(
|
infos = request.get_cookie(
|
||||||
f"session.{Session.actionsmap_name}", secret=Session.secret, default={}
|
f"session.{Session.cookie_name}", secret=Session.secret, default={}
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
if not raise_if_no_session_exists:
|
if not raise_if_no_session_exists:
|
||||||
|
@ -271,8 +271,8 @@ class Session:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_infos():
|
def delete_infos():
|
||||||
|
|
||||||
response.set_cookie(f"session.{Session.actionsmap_name}", "", max_age=-1)
|
response.set_cookie(f"session.{Session.cookie_name}", "", max_age=-1)
|
||||||
response.delete_cookie(f"session.{Session.actionsmap_name}")
|
response.delete_cookie(f"session.{Session.cookie_name}")
|
||||||
|
|
||||||
|
|
||||||
class _ActionsMapPlugin(object):
|
class _ActionsMapPlugin(object):
|
||||||
|
@ -294,7 +294,7 @@ class _ActionsMapPlugin(object):
|
||||||
|
|
||||||
self.actionsmap = actionsmap
|
self.actionsmap = actionsmap
|
||||||
self.log_queues = log_queues
|
self.log_queues = log_queues
|
||||||
Session.actionsmap_name = actionsmap.name
|
Session.cookie_name = actionsmap.cookie_name
|
||||||
|
|
||||||
def setup(self, app):
|
def setup(self, app):
|
||||||
"""Setup plugin on the application
|
"""Setup plugin on the application
|
||||||
|
@ -734,9 +734,9 @@ class Interface:
|
||||||
|
|
||||||
type = "api"
|
type = "api"
|
||||||
|
|
||||||
def __init__(self, routes={}):
|
def __init__(self, routes={}, actionsmap=None):
|
||||||
|
|
||||||
actionsmap = ActionsMap(ActionsMapParser())
|
actionsmap = ActionsMap(actionsmap, ActionsMapParser())
|
||||||
|
|
||||||
# Attempt to retrieve log queues from an APIQueueHandler
|
# Attempt to retrieve log queues from an APIQueueHandler
|
||||||
handler = log.getHandlersByClass(APIQueueHandler, limit=1)
|
handler = log.getHandlersByClass(APIQueueHandler, limit=1)
|
||||||
|
|
|
@ -461,12 +461,13 @@ class Interface:
|
||||||
|
|
||||||
type = "cli"
|
type = "cli"
|
||||||
|
|
||||||
def __init__(self, top_parser=None, load_only_category=None):
|
def __init__(self, top_parser=None, load_only_category=None, actionsmap=None, locales_dir=None):
|
||||||
|
|
||||||
# Set user locale
|
# Set user locale
|
||||||
m18n.set_locale(get_locale())
|
m18n.set_locale(get_locale())
|
||||||
|
|
||||||
self.actionsmap = ActionsMap(
|
self.actionsmap = ActionsMap(
|
||||||
|
actionsmap,
|
||||||
ActionsMapParser(top_parser=top_parser),
|
ActionsMapParser(top_parser=top_parser),
|
||||||
load_only_category=load_only_category,
|
load_only_category=load_only_category,
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,5 +3,4 @@ addopts = --cov=moulinette -s -v --no-cov-on-fail
|
||||||
norecursedirs = dist doc build .tox .eggs
|
norecursedirs = dist doc build .tox .eggs
|
||||||
testpaths = test/
|
testpaths = test/
|
||||||
env =
|
env =
|
||||||
MOULINETTE_LOCALES_DIR = {PWD}/locales
|
|
||||||
TESTS_RUN = True
|
TESTS_RUN = True
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -5,7 +5,6 @@ import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from moulinette import env
|
|
||||||
|
|
||||||
version = (
|
version = (
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
|
@ -15,8 +14,6 @@ version = (
|
||||||
.strip()
|
.strip()
|
||||||
)
|
)
|
||||||
|
|
||||||
LOCALES_DIR = env["LOCALES_DIR"]
|
|
||||||
|
|
||||||
# Extend installation
|
# Extend installation
|
||||||
locale_files = []
|
locale_files = []
|
||||||
|
|
||||||
|
@ -62,7 +59,7 @@ setup(
|
||||||
url="https://yunohost.org",
|
url="https://yunohost.org",
|
||||||
license="AGPL",
|
license="AGPL",
|
||||||
packages=find_packages(exclude=["test"]),
|
packages=find_packages(exclude=["test"]),
|
||||||
data_files=[(LOCALES_DIR, locale_files)],
|
data_files=[("/usr/share/moulinette/locales", locale_files)],
|
||||||
python_requires=">=3.7.*, <3.10",
|
python_requires=">=3.7.*, <3.10",
|
||||||
install_requires=install_deps,
|
install_requires=install_deps,
|
||||||
tests_require=test_deps,
|
tests_require=test_deps,
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
# Global parameters #
|
# Global parameters #
|
||||||
#############################
|
#############################
|
||||||
_global:
|
_global:
|
||||||
name: moulitest
|
namespace: moulitest
|
||||||
|
cookie_name: moulitest
|
||||||
authentication:
|
authentication:
|
||||||
api: dummy
|
api: dummy
|
||||||
cli: dummy
|
cli: dummy
|
||||||
|
|
|
@ -1,24 +1,15 @@
|
||||||
"""Pytest fixtures for testing."""
|
"""Pytest fixtures for testing."""
|
||||||
|
|
||||||
|
import sys
|
||||||
import toml
|
import toml
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def patch_init(moulinette):
|
|
||||||
"""Configure moulinette to use the YunoHost namespace."""
|
|
||||||
old_init = moulinette.core.Moulinette18n.__init__
|
|
||||||
|
|
||||||
def monkey_path_i18n_init(self, package, default_locale="en"):
|
|
||||||
old_init(self, package, default_locale)
|
|
||||||
self.load_namespace("moulinette")
|
|
||||||
|
|
||||||
moulinette.core.Moulinette18n.__init__ = monkey_path_i18n_init
|
|
||||||
|
|
||||||
|
|
||||||
def patch_translate(moulinette):
|
def patch_translate(moulinette):
|
||||||
"""Configure translator to raise errors when there are missing keys."""
|
"""Configure translator to raise errors when there are missing keys."""
|
||||||
old_translate = moulinette.core.Translator.translate
|
old_translate = moulinette.core.Translator.translate
|
||||||
|
@ -38,7 +29,7 @@ def patch_translate(moulinette):
|
||||||
moulinette.core.Moulinette18n.g = new_m18nn
|
moulinette.core.Moulinette18n.g = new_m18nn
|
||||||
|
|
||||||
|
|
||||||
def patch_logging(moulinette):
|
def logging_configuration(moulinette):
|
||||||
"""Configure logging to use the custom logger."""
|
"""Configure logging to use the custom logger."""
|
||||||
handlers = set(["tty", "api"])
|
handlers = set(["tty", "api"])
|
||||||
root_handlers = set(handlers)
|
root_handlers = set(handlers)
|
||||||
|
@ -85,27 +76,28 @@ def patch_lock(moulinette):
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
def moulinette(tmp_path_factory):
|
def moulinette(tmp_path_factory):
|
||||||
|
|
||||||
import moulinette
|
import moulinette
|
||||||
|
import moulinette.core
|
||||||
|
from moulinette.utils.log import configure_logging
|
||||||
|
|
||||||
# Can't call the namespace just 'test' because
|
# Can't call the namespace just 'test' because
|
||||||
# that would lead to some "import test" not importing the right stuff
|
# that would lead to some "import test" not importing the right stuff
|
||||||
namespace = "moulitest"
|
namespace = "moulitest"
|
||||||
tmp_cache = str(tmp_path_factory.mktemp("cache"))
|
tmp_dir = str(tmp_path_factory.mktemp(namespace))
|
||||||
tmp_data = str(tmp_path_factory.mktemp("data"))
|
shutil.copy("./test/actionsmap/moulitest.yml", f"{tmp_dir}/moulitest.yml")
|
||||||
tmp_lib = str(tmp_path_factory.mktemp("lib"))
|
shutil.copytree("./test/src", f"{tmp_dir}/lib/{namespace}/")
|
||||||
moulinette.env["CACHE_DIR"] = tmp_cache
|
shutil.copytree("./test/locales", f"{tmp_dir}/locales")
|
||||||
moulinette.env["DATA_DIR"] = tmp_data
|
sys.path.insert(0, f"{tmp_dir}/lib")
|
||||||
moulinette.env["LIB_DIR"] = tmp_lib
|
|
||||||
shutil.copytree("./test/actionsmap", "%s/actionsmap" % tmp_data)
|
|
||||||
shutil.copytree("./test/src", "%s/%s" % (tmp_lib, namespace))
|
|
||||||
shutil.copytree("./test/locales", "%s/%s/locales" % (tmp_lib, namespace))
|
|
||||||
|
|
||||||
patch_init(moulinette)
|
|
||||||
patch_translate(moulinette)
|
patch_translate(moulinette)
|
||||||
patch_lock(moulinette)
|
patch_lock(moulinette)
|
||||||
logging = patch_logging(moulinette)
|
|
||||||
|
|
||||||
moulinette.init(logging_config=logging, _from_source=False)
|
configure_logging(logging_configuration(moulinette))
|
||||||
|
moulinette.m18n.set_locales_dir(f"{tmp_dir}/locales")
|
||||||
|
|
||||||
|
# Dirty hack to pass this path to Api() and Cli() init later
|
||||||
|
moulinette._actionsmap_path = f"{tmp_dir}/moulitest.yml"
|
||||||
|
|
||||||
return moulinette
|
return moulinette
|
||||||
|
|
||||||
|
@ -125,7 +117,7 @@ def moulinette_webapi(moulinette):
|
||||||
|
|
||||||
from moulinette.interfaces.api import Interface as Api
|
from moulinette.interfaces.api import Interface as Api
|
||||||
|
|
||||||
return TestApp(Api(routes={})._app)
|
return TestApp(Api(routes={}, actionsmap=moulinette._actionsmap_path)._app)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -142,7 +134,7 @@ def moulinette_cli(moulinette, mocker):
|
||||||
mocker.patch("os.isatty", return_value=True)
|
mocker.patch("os.isatty", return_value=True)
|
||||||
from moulinette.interfaces.cli import Interface as Cli
|
from moulinette.interfaces.cli import Interface as Cli
|
||||||
|
|
||||||
cli = Cli(top_parser=parser)
|
cli = Cli(top_parser=parser, actionsmap=moulinette._actionsmap_path)
|
||||||
mocker.stopall()
|
mocker.stopall()
|
||||||
|
|
||||||
return cli
|
return cli
|
||||||
|
|
|
@ -164,7 +164,7 @@ def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
|
||||||
|
|
||||||
from moulinette.interfaces.api import ActionsMapParser
|
from moulinette.interfaces.api import ActionsMapParser
|
||||||
|
|
||||||
amap = ActionsMap(ActionsMapParser())
|
amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
|
||||||
|
|
||||||
with pytest.raises(MoulinetteError) as exception:
|
with pytest.raises(MoulinetteError) as exception:
|
||||||
amap.get_authenticator("unknown")
|
amap.get_authenticator("unknown")
|
||||||
|
@ -233,9 +233,9 @@ def test_actions_map_api():
|
||||||
from moulinette.interfaces.api import ActionsMapParser
|
from moulinette.interfaces.api import ActionsMapParser
|
||||||
|
|
||||||
parser = ActionsMapParser()
|
parser = ActionsMapParser()
|
||||||
amap = ActionsMap(parser)
|
amap = ActionsMap("test/actionsmap/moulitest.yml", parser)
|
||||||
|
|
||||||
assert amap.main_namespace == "moulitest"
|
assert amap.namespace == "moulitest"
|
||||||
assert amap.default_authentication == "dummy"
|
assert amap.default_authentication == "dummy"
|
||||||
assert ("GET", "/test-auth/default") in amap.parser.routes
|
assert ("GET", "/test-auth/default") in amap.parser.routes
|
||||||
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
|
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
|
||||||
|
@ -248,7 +248,7 @@ def test_actions_map_api():
|
||||||
def test_actions_map_import_error(mocker):
|
def test_actions_map_import_error(mocker):
|
||||||
from moulinette.interfaces.api import ActionsMapParser
|
from moulinette.interfaces.api import ActionsMapParser
|
||||||
|
|
||||||
amap = ActionsMap(ActionsMapParser())
|
amap = ActionsMap("test/actionsmap/moulitest.yml", ActionsMapParser())
|
||||||
|
|
||||||
from moulinette.core import MoulinetteLock
|
from moulinette.core import MoulinetteLock
|
||||||
|
|
||||||
|
@ -287,9 +287,9 @@ def test_actions_map_cli():
|
||||||
)
|
)
|
||||||
|
|
||||||
parser = ActionsMapParser(top_parser=top_parser)
|
parser = ActionsMapParser(top_parser=top_parser)
|
||||||
amap = ActionsMap(parser)
|
amap = ActionsMap("test/actionsmap/moulitest.yml", parser)
|
||||||
|
|
||||||
assert amap.main_namespace == "moulitest"
|
assert amap.namespace == "moulitest"
|
||||||
assert amap.default_authentication == "dummy"
|
assert amap.default_authentication == "dummy"
|
||||||
assert "testauth" in amap.parser._subparsers.choices
|
assert "testauth" in amap.parser._subparsers.choices
|
||||||
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
|
assert "none" in amap.parser._subparsers.choices["testauth"]._actions[1].choices
|
||||||
|
|
Loading…
Add table
Reference in a new issue