Repository: incubator-ariatosca Updated Branches: refs/heads/ARIA-48-aria-cli 3bff159e6 -> 79f5d78eb
reviewed helptexts module; renamed cli.cli package to cli.core Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/79f5d78e Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/79f5d78e Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/79f5d78e Branch: refs/heads/ARIA-48-aria-cli Commit: 79f5d78ebe31327e7635ae75551ba636ac5fd1dc Parents: 3bff159 Author: Ran Ziv <[email protected]> Authored: Tue Apr 18 16:55:52 2017 +0300 Committer: Ran Ziv <[email protected]> Committed: Tue Apr 18 16:55:52 2017 +0300 ---------------------------------------------------------------------- aria/cli/cli/__init__.py | 14 - aria/cli/cli/aria.py | 439 ---------------------------- aria/cli/cli/helptexts.py | 57 ---- aria/cli/commands/executions.py | 5 +- aria/cli/commands/logs.py | 2 +- aria/cli/commands/node_templates.py | 4 +- aria/cli/commands/nodes.py | 2 +- aria/cli/commands/plugins.py | 4 +- aria/cli/commands/reset.py | 4 +- aria/cli/commands/service_templates.py | 2 +- aria/cli/commands/services.py | 5 +- aria/cli/commands/workflows.py | 2 +- aria/cli/core/__init__.py | 14 + aria/cli/core/aria.py | 429 +++++++++++++++++++++++++++ aria/cli/helptexts.py | 49 ++++ aria/cli/main.py | 2 +- 16 files changed, 509 insertions(+), 525 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/cli/__init__.py ---------------------------------------------------------------------- diff --git a/aria/cli/cli/__init__.py b/aria/cli/cli/__init__.py deleted file mode 100644 index ae1e83e..0000000 --- a/aria/cli/cli/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/cli/aria.py ---------------------------------------------------------------------- diff --git a/aria/cli/cli/aria.py b/aria/cli/cli/aria.py deleted file mode 100644 index 548be23..0000000 --- a/aria/cli/cli/aria.py +++ /dev/null @@ -1,439 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import os -import sys -import difflib -import StringIO -import traceback -from functools import wraps - -import click - -from ..env import ( - env, - logger -) -from ..cli import helptexts -from ..inputs import inputs_to_dict -from ..constants import DEFAULT_SERVICE_TEMPLATE_FILENAME -from ...utils.exceptions import get_exception_as_string -from ... import __version__ - - -CLICK_CONTEXT_SETTINGS = dict( - help_option_names=['-h', '--help'], - token_normalize_func=lambda param: param.lower()) - - -class MutuallyExclusiveOption(click.Option): - """Makes options mutually exclusive. The option must pass a `cls` argument - with this class name and a `mutually_exclusive` argument with a list of - argument names it is mutually exclusive with. - - NOTE: All mutually exclusive options must use this. It's not enough to - use it in just one of the options. - """ - - def __init__(self, *args, **kwargs): - self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', [])) - self.mutuality_error_message = \ - kwargs.pop('mutuality_error_message', - helptexts.DEFAULT_MUTUALITY_MESSAGE) - self.mutuality_string = ', '.join(self.mutually_exclusive) - if self.mutually_exclusive: - help = kwargs.get('help', '') - kwargs['help'] = ( - '{0}. This argument is mutually exclusive with ' - 'arguments: [{1}] ({2})'.format( - help, - self.mutuality_string, - self.mutuality_error_message)) - super(MutuallyExclusiveOption, self).__init__(*args, **kwargs) - - def handle_parse_result(self, ctx, opts, args): - if self.mutually_exclusive.intersection(opts) and self.name in opts: - raise click.UsageError( - 'Illegal usage: `{0}` is mutually exclusive with ' - 'arguments: [{1}] ({2}).'.format( - self.name, - self.mutuality_string, - self.mutuality_error_message)) - return super(MutuallyExclusiveOption, self).handle_parse_result( - ctx, opts, args) - - -def _format_version_data(version, - prefix=None, - suffix=None, - infix=None): - all_data = dict(version=version) - all_data['prefix'] = prefix or '' - all_data['suffix'] = suffix or '' - all_data['infix'] = infix or '' - output = StringIO.StringIO() - output.write('{prefix}{version}'.format(**all_data)) - output.write('{suffix}'.format(**all_data)) - return output.getvalue() - - -def show_version(ctx, param, value): - if not value: - return - - cli_version = _format_version_data( - __version__, - prefix='ARIA CLI ', - infix=' ' * 5, - suffix='') - - logger.info(cli_version) - ctx.exit() - - -def inputs_callback(ctx, param, value): - """Allow to pass any inputs we provide to a command as - processed inputs instead of having to call `inputs_to_dict` - inside the command. - - `@aria.options.inputs` already calls this callback so that - every time you use the option it returns the inputs as a - dictionary. - """ - if not value: - return {} - - return inputs_to_dict(value) - - -def set_verbosity_level(ctx, param, value): - if not value: - return - - env.logging.verbosity_level = value - - -def set_cli_except_hook(): - - def recommend(possible_solutions): - logger.info('Possible solutions:') - for solution in possible_solutions: - logger.info(' - {0}'.format(solution)) - - def new_excepthook(tpe, value, trace): - if env.logging.is_high_verbose_level(): - # log error including traceback - logger.error(get_exception_as_string(tpe, value, trace)) - else: - # write the full error to the log file - with open(env.logging.log_file, 'a') as log_file: - traceback.print_exception( - etype=tpe, - value=value, - tb=trace, - file=log_file) - # print only the error message - print value - - if hasattr(value, 'possible_solutions'): - recommend(getattr(value, 'possible_solutions')) - - sys.excepthook = new_excepthook - - -def pass_logger(func): - """Simply passes the logger to a command. - """ - # Wraps here makes sure the original docstring propagates to click - @wraps(func) - def wrapper(*args, **kwargs): - return func(logger=logger, *args, **kwargs) - - return wrapper - - -def pass_plugin_manager(func): - """Simply passes the plugin manager to a command. - """ - # Wraps here makes sure the original docstring propagates to click - @wraps(func) - def wrapper(*args, **kwargs): - return func(plugin_manager=env.plugin_manager, *args, **kwargs) - - return wrapper - - -def pass_model_storage(func): - """Simply passes the model storage to a command. - """ - # Wraps here makes sure the original docstring propagates to click - @wraps(func) - def wrapper(*args, **kwargs): - return func(model_storage=env.model_storage, *args, **kwargs) - - return wrapper - - -def pass_resource_storage(func): - """Simply passes the resource storage to a command. - """ - # Wraps here makes sure the original docstring propagates to click - @wraps(func) - def wrapper(*args, **kwargs): - return func(resource_storage=env.resource_storage, *args, **kwargs) - - return wrapper - - -def pass_context(func): - """Make click context ARIA specific - - This exists purely for aesthetic reasons, otherwise - Some decorators are called `@click.something` instead of - `@aria.something` - """ - return click.pass_context(func) - - -class AliasedGroup(click.Group): - def __init__(self, *args, **kwargs): - self.max_suggestions = kwargs.pop("max_suggestions", 3) - self.cutoff = kwargs.pop("cutoff", 0.5) - super(AliasedGroup, self).__init__(*args, **kwargs) - - def get_command(self, ctx, cmd_name): - cmd = click.Group.get_command(self, ctx, cmd_name) - if cmd is not None: - return cmd - matches = \ - [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] - if not matches: - return None - elif len(matches) == 1: - return click.Group.get_command(self, ctx, matches[0]) - ctx.fail('Too many matches: {0}'.format(', '.join(sorted(matches)))) - - def resolve_command(self, ctx, args): - """Override clicks ``resolve_command`` method - and appends *Did you mean ...* suggestions - to the raised exception message. - """ - try: - return super(AliasedGroup, self).resolve_command(ctx, args) - except click.exceptions.UsageError as error: - error_msg = str(error) - original_cmd_name = click.utils.make_str(args[0]) - matches = difflib.get_close_matches( - original_cmd_name, - self.list_commands(ctx), - self.max_suggestions, - self.cutoff) - if matches: - error_msg += '{0}{0}Did you mean one of these?{0} {1}'.format( - os.linesep, - '{0} '.format(os.linesep).join(matches, )) - raise click.exceptions.UsageError(error_msg, error.ctx) - - -def group(name): - """Allow to create a group with a default click context - and a cls for click's `didyoueamn` without having to repeat - it for every group. - """ - return click.group( - name=name, - context_settings=CLICK_CONTEXT_SETTINGS, - cls=AliasedGroup) - - -def command(*args, **kwargs): - """Make Click commands ARIA specific - - This exists purely for aesthetical reasons, otherwise - Some decorators are called `@click.something` instead of - `@aria.something` - """ - return click.command(*args, **kwargs) - - -def argument(*args, **kwargs): - """Make Click arguments ARIA specific - - This exists purely for aesthetic reasons, otherwise - Some decorators are called `@click.something` instead of - `@aria.something` - """ - return click.argument(*args, **kwargs) - - -class Options(object): - def __init__(self): - """The options api is nicer when you use each option by calling - `@aria.options.some_option` instead of `@aria.some_option`. - - Note that some options are attributes and some are static methods. - The reason for that is that we want to be explicit regarding how - a developer sees an option. It it can receive arguments, it's a - method - if not, it's an attribute. - """ - self.version = click.option( - '--version', - is_flag=True, - callback=show_version, - expose_value=False, - is_eager=True, - help=helptexts.VERSION) - - self.inputs = click.option( - '-i', - '--inputs', - multiple=True, - callback=inputs_callback, - help=helptexts.INPUTS) - - self.json_output = click.option( - '--json-output', - is_flag=True, - help=helptexts.JSON_OUTPUT) - - self.dry_execution = click.option( - '--dry', - is_flag=True, - help=helptexts.DRY_EXECUTION) - - self.reset_config = click.option( - '--reset-config', - is_flag=True, - help=helptexts.RESET_CONFIG) - - self.enable_colors = click.option( - '--enable-colors', - is_flag=True, - default=False, - help=helptexts.ENABLE_COLORS) - - self.node_name = click.option( - '-n', - '--node-name', - required=False, - help=helptexts.NODE_NAME) - - self.descending = click.option( - '--descending', - required=False, - is_flag=True, - default=False, - help=helptexts.DESCENDING) - - self.service_template_filename = click.option( - '-n', - '--service-template-filename', - default=DEFAULT_SERVICE_TEMPLATE_FILENAME, - help=helptexts.SERVICE_TEMPLATE_FILENAME) - - @staticmethod - def verbose(expose_value=False): - return click.option( - '-v', - '--verbose', - count=True, - callback=set_verbosity_level, - expose_value=expose_value, - is_eager=True, - help=helptexts.VERBOSE) - - @staticmethod - def force(help): - return click.option( - '-f', - '--force', - is_flag=True, - help=help) - - @staticmethod - def task_max_attempts(default=1): - return click.option( - '--task-max-attempts', - type=int, - default=default, - help=helptexts.TASK_MAX_ATTEMPTS.format(default)) - - @staticmethod - def sort_by(default='created_at'): - return click.option( - '--sort-by', - required=False, - default=default, - help=helptexts.SORT_BY) - - @staticmethod - def task_retry_interval(default=1): - return click.option( - '--task-retry-interval', - type=int, - default=default, - help=helptexts.TASK_RETRY_INTERVAL.format(default)) - - @staticmethod - def service_id(required=False): - return click.option( - '-s', - '--service-id', - required=required, - help=helptexts.SERVICE_ID) - - @staticmethod - def execution_id(required=False): - return click.option( - '-e', - '--execution-id', - required=required, - help=helptexts.EXECUTION_ID) - - @staticmethod - def service_template_id(required=False): - return click.option( - '-t', - '--service-template-id', - required=required, - help=helptexts.SERVICE_TEMPLATE_ID) - - @staticmethod - def service_template_path(required=False): - return click.option( - '-p', - '--service-template-path', - required=required, - type=click.Path(exists=True)) - - @staticmethod - def service_name(required=False): - return click.option( - '-s', - '--service-name', - required=required, - help=helptexts.SERVICE_ID) - - @staticmethod - def service_template_name(required=False): - return click.option( - '-t', - '--service-template-name', - required=required, - help=helptexts.SERVICE_ID) - - -options = Options() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/cli/helptexts.py ---------------------------------------------------------------------- diff --git a/aria/cli/cli/helptexts.py b/aria/cli/cli/helptexts.py deleted file mode 100644 index f8b315c..0000000 --- a/aria/cli/cli/helptexts.py +++ /dev/null @@ -1,57 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -VERBOSE = \ - "Show verbose output. You can supply this up to three times (i.e. -vvv)" -VERSION = "Display the version and exit" - -INPUTS_PARAMS_USAGE = ( - '(Can be provided as wildcard based paths ' - '(*.yaml, /my_inputs/, etc..) to YAML files, a JSON string or as ' - 'key1=value1;key2=value2). This argument can be used multiple times' -) - -SERVICE_TEMPLATE_PATH = "The path to the application's service template file" -SERVICE_TEMPLATE_ID = "The unique identifier for the service template" - -FORCE_RESET = "Confirmation for resetting ARIA's working directory" -RESET_CONFIG = "Reset ARIA's user configuration" -ENABLE_COLORS = "Enable colors in logger (use --hard when working with" \ - " an initialized environment) [default: False]" - -DRY_EXECUTION = "Execute a workflow dry run (prints operations information without causing side " \ - "effects)" -SERVICE_TEMPLATE_FILENAME = ( - "The name of the archive's main service template file. " - "This is only relevant if uploading an archive") -INPUTS = "Inputs for the service {0}".format(INPUTS_PARAMS_USAGE) -PARAMETERS = "Parameters for the workflow {0}".format(INPUTS_PARAMS_USAGE) -TASK_RETRY_INTERVAL = \ - "How long of a minimal interval should occur between task retry attempts [default: {0}]" -TASK_MAX_ATTEMPTS = \ - "How many times should a task be attempted in case of failures [default: {0}]" - -JSON_OUTPUT = "Output events in a consumable JSON format" - -SERVICE_ID = "The unique identifier for the service" -EXECUTION_ID = "The unique identifier for the execution" -IGNORE_AVAILABLE_NODES = "Delete the service even if it has available nodes" - -NODE_NAME = "The node's name" - -DEFAULT_MUTUALITY_MESSAGE = 'Cannot be used simultaneously' - -SORT_BY = "Key for sorting the list" -DESCENDING = "Sort list in descending order [default: False]" http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/executions.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/executions.py b/aria/cli/commands/executions.py index 730fd29..cd12ead 100644 --- a/aria/cli/commands/executions.py +++ b/aria/cli/commands/executions.py @@ -15,9 +15,10 @@ import os +from .. import helptexts from .. import utils +from ..core import aria from ..table import print_data -from ..cli import aria from ...modeling.models import Execution from ...orchestrator.workflow_runner import WorkflowRunner from ...orchestrator.workflows.executor.dry import DryExecutor @@ -103,7 +104,7 @@ def list(service_name, short_help='Execute a workflow') @aria.argument('workflow-name') @aria.options.service_name(required=True) [email protected] [email protected](help=helptexts.EXECUTION_INPUTS) @aria.options.dry_execution @aria.options.task_max_attempts() @aria.options.task_retry_interval() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/logs.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/logs.py b/aria/cli/commands/logs.py index f8873cd..8888fef 100644 --- a/aria/cli/commands/logs.py +++ b/aria/cli/commands/logs.py @@ -14,7 +14,7 @@ # limitations under the License. from .. import utils -from ..cli import aria +from ..core import aria @aria.group(name='logs') http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/node_templates.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/node_templates.py b/aria/cli/commands/node_templates.py index cf50ceb..b63b630 100644 --- a/aria/cli/commands/node_templates.py +++ b/aria/cli/commands/node_templates.py @@ -13,9 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..table import print_data from .. import utils -from ..cli import aria +from ..core import aria +from ..table import print_data NODE_TEMPLATE_COLUMNS = ['id', 'name', 'description', 'service_template_name', 'type_name'] http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/nodes.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/nodes.py b/aria/cli/commands/nodes.py index fd65e24..b1f2acc 100644 --- a/aria/cli/commands/nodes.py +++ b/aria/cli/commands/nodes.py @@ -14,7 +14,7 @@ # limitations under the License. from .. import utils -from ..cli import aria +from ..core import aria from ..table import print_data http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/plugins.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/plugins.py b/aria/cli/commands/plugins.py index 9e7d449..680284f 100644 --- a/aria/cli/commands/plugins.py +++ b/aria/cli/commands/plugins.py @@ -15,9 +15,9 @@ import zipfile -from ..table import print_data -from ..cli import aria +from ..core import aria from ..exceptions import AriaCliError +from ..table import print_data from ..utils import storage_sort_param http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/reset.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/reset.py b/aria/cli/commands/reset.py index 775f555..1fe0714 100644 --- a/aria/cli/commands/reset.py +++ b/aria/cli/commands/reset.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..cli import aria -from ..cli import helptexts +from .. import helptexts +from ..core import aria from ..env import env from ..exceptions import AriaCliError http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/service_templates.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/service_templates.py b/aria/cli/commands/service_templates.py index 8e0e91c..93dc188 100644 --- a/aria/cli/commands/service_templates.py +++ b/aria/cli/commands/service_templates.py @@ -19,7 +19,7 @@ import os from .. import utils from .. import csar from .. import service_template_utils -from ..cli import aria +from ..core import aria from ..table import print_data from ..exceptions import AriaCliError from ..utils import handle_storage_exception http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/services.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/services.py b/aria/cli/commands/services.py index 78899c5..4728509 100644 --- a/aria/cli/commands/services.py +++ b/aria/cli/commands/services.py @@ -18,7 +18,8 @@ import os from StringIO import StringIO from . import service_templates -from ..cli import aria, helptexts +from .. import helptexts +from ..core import aria from ..exceptions import AriaCliError from ..table import print_data from ..utils import storage_sort_param, handle_storage_exception @@ -74,7 +75,7 @@ def list(service_template_name, short_help='Create a services') @aria.argument('service-name', required=False) @aria.options.service_template_name(required=True) [email protected] [email protected](help=helptexts.SERVICE_INPUTS) @aria.options.verbose() @aria.pass_model_storage @aria.pass_resource_storage http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/commands/workflows.py ---------------------------------------------------------------------- diff --git a/aria/cli/commands/workflows.py b/aria/cli/commands/workflows.py index 72dea5b..d380fac 100644 --- a/aria/cli/commands/workflows.py +++ b/aria/cli/commands/workflows.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ..core import aria from ..table import print_data -from ..cli import aria from ..exceptions import AriaCliError WORKFLOW_COLUMNS = ['name', 'service_template_name', 'service_name'] http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/core/__init__.py ---------------------------------------------------------------------- diff --git a/aria/cli/core/__init__.py b/aria/cli/core/__init__.py new file mode 100644 index 0000000..ae1e83e --- /dev/null +++ b/aria/cli/core/__init__.py @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/core/aria.py ---------------------------------------------------------------------- diff --git a/aria/cli/core/aria.py b/aria/cli/core/aria.py new file mode 100644 index 0000000..cd1036e --- /dev/null +++ b/aria/cli/core/aria.py @@ -0,0 +1,429 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +import sys +import difflib +import StringIO +import traceback +from functools import wraps + +import click + +from ..env import ( + env, + logger +) +from .. import helptexts +from ..inputs import inputs_to_dict +from ..constants import DEFAULT_SERVICE_TEMPLATE_FILENAME +from ...utils.exceptions import get_exception_as_string +from ... import __version__ + + +CLICK_CONTEXT_SETTINGS = dict( + help_option_names=['-h', '--help'], + token_normalize_func=lambda param: param.lower()) + + +class MutuallyExclusiveOption(click.Option): + """Makes options mutually exclusive. The option must pass a `cls` argument + with this class name and a `mutually_exclusive` argument with a list of + argument names it is mutually exclusive with. + + NOTE: All mutually exclusive options must use this. It's not enough to + use it in just one of the options. + """ + + def __init__(self, *args, **kwargs): + self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', [])) + self.mutuality_error_message = \ + kwargs.pop('mutuality_error_message', + helptexts.DEFAULT_MUTUALITY_MESSAGE) + self.mutuality_string = ', '.join(self.mutually_exclusive) + if self.mutually_exclusive: + help = kwargs.get('help', '') + kwargs['help'] = ( + '{0}. This argument is mutually exclusive with ' + 'arguments: [{1}] ({2})'.format( + help, + self.mutuality_string, + self.mutuality_error_message)) + super(MutuallyExclusiveOption, self).__init__(*args, **kwargs) + + def handle_parse_result(self, ctx, opts, args): + if self.mutually_exclusive.intersection(opts) and self.name in opts: + raise click.UsageError( + 'Illegal usage: `{0}` is mutually exclusive with ' + 'arguments: [{1}] ({2}).'.format( + self.name, + self.mutuality_string, + self.mutuality_error_message)) + return super(MutuallyExclusiveOption, self).handle_parse_result( + ctx, opts, args) + + +def _format_version_data(version, + prefix=None, + suffix=None, + infix=None): + all_data = dict(version=version) + all_data['prefix'] = prefix or '' + all_data['suffix'] = suffix or '' + all_data['infix'] = infix or '' + output = StringIO.StringIO() + output.write('{prefix}{version}'.format(**all_data)) + output.write('{suffix}'.format(**all_data)) + return output.getvalue() + + +def show_version(ctx, param, value): + if not value: + return + + cli_version = _format_version_data( + __version__, + prefix='ARIA CLI ', + infix=' ' * 5, + suffix='') + + logger.info(cli_version) + ctx.exit() + + +def inputs_callback(ctx, param, value): + """Allow to pass any inputs we provide to a command as + processed inputs instead of having to call `inputs_to_dict` + inside the command. + + `@aria.options.inputs` already calls this callback so that + every time you use the option it returns the inputs as a + dictionary. + """ + if not value: + return {} + + return inputs_to_dict(value) + + +def set_verbosity_level(ctx, param, value): + if not value: + return + + env.logging.verbosity_level = value + + +def set_cli_except_hook(): + + def recommend(possible_solutions): + logger.info('Possible solutions:') + for solution in possible_solutions: + logger.info(' - {0}'.format(solution)) + + def new_excepthook(tpe, value, trace): + if env.logging.is_high_verbose_level(): + # log error including traceback + logger.error(get_exception_as_string(tpe, value, trace)) + else: + # write the full error to the log file + with open(env.logging.log_file, 'a') as log_file: + traceback.print_exception( + etype=tpe, + value=value, + tb=trace, + file=log_file) + # print only the error message + print value + + if hasattr(value, 'possible_solutions'): + recommend(getattr(value, 'possible_solutions')) + + sys.excepthook = new_excepthook + + +def pass_logger(func): + """Simply passes the logger to a command. + """ + # Wraps here makes sure the original docstring propagates to click + @wraps(func) + def wrapper(*args, **kwargs): + return func(logger=logger, *args, **kwargs) + + return wrapper + + +def pass_plugin_manager(func): + """Simply passes the plugin manager to a command. + """ + # Wraps here makes sure the original docstring propagates to click + @wraps(func) + def wrapper(*args, **kwargs): + return func(plugin_manager=env.plugin_manager, *args, **kwargs) + + return wrapper + + +def pass_model_storage(func): + """Simply passes the model storage to a command. + """ + # Wraps here makes sure the original docstring propagates to click + @wraps(func) + def wrapper(*args, **kwargs): + return func(model_storage=env.model_storage, *args, **kwargs) + + return wrapper + + +def pass_resource_storage(func): + """Simply passes the resource storage to a command. + """ + # Wraps here makes sure the original docstring propagates to click + @wraps(func) + def wrapper(*args, **kwargs): + return func(resource_storage=env.resource_storage, *args, **kwargs) + + return wrapper + + +def pass_context(func): + """Make click context ARIA specific + + This exists purely for aesthetic reasons, otherwise + Some decorators are called `@click.something` instead of + `@aria.something` + """ + return click.pass_context(func) + + +class AliasedGroup(click.Group): + def __init__(self, *args, **kwargs): + self.max_suggestions = kwargs.pop("max_suggestions", 3) + self.cutoff = kwargs.pop("cutoff", 0.5) + super(AliasedGroup, self).__init__(*args, **kwargs) + + def get_command(self, ctx, cmd_name): + cmd = click.Group.get_command(self, ctx, cmd_name) + if cmd is not None: + return cmd + matches = \ + [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] + if not matches: + return None + elif len(matches) == 1: + return click.Group.get_command(self, ctx, matches[0]) + ctx.fail('Too many matches: {0}'.format(', '.join(sorted(matches)))) + + def resolve_command(self, ctx, args): + """Override clicks ``resolve_command`` method + and appends *Did you mean ...* suggestions + to the raised exception message. + """ + try: + return super(AliasedGroup, self).resolve_command(ctx, args) + except click.exceptions.UsageError as error: + error_msg = str(error) + original_cmd_name = click.utils.make_str(args[0]) + matches = difflib.get_close_matches( + original_cmd_name, + self.list_commands(ctx), + self.max_suggestions, + self.cutoff) + if matches: + error_msg += '{0}{0}Did you mean one of these?{0} {1}'.format( + os.linesep, + '{0} '.format(os.linesep).join(matches, )) + raise click.exceptions.UsageError(error_msg, error.ctx) + + +def group(name): + """Allow to create a group with a default click context + and a cls for click's `didyoueamn` without having to repeat + it for every group. + """ + return click.group( + name=name, + context_settings=CLICK_CONTEXT_SETTINGS, + cls=AliasedGroup) + + +def command(*args, **kwargs): + """Make Click commands ARIA specific + + This exists purely for aesthetical reasons, otherwise + Some decorators are called `@click.something` instead of + `@aria.something` + """ + return click.command(*args, **kwargs) + + +def argument(*args, **kwargs): + """Make Click arguments ARIA specific + + This exists purely for aesthetic reasons, otherwise + Some decorators are called `@click.something` instead of + `@aria.something` + """ + return click.argument(*args, **kwargs) + + +class Options(object): + def __init__(self): + """The options api is nicer when you use each option by calling + `@aria.options.some_option` instead of `@aria.some_option`. + + Note that some options are attributes and some are static methods. + The reason for that is that we want to be explicit regarding how + a developer sees an option. It it can receive arguments, it's a + method - if not, it's an attribute. + """ + self.version = click.option( + '--version', + is_flag=True, + callback=show_version, + expose_value=False, + is_eager=True, + help=helptexts.VERSION) + + self.json_output = click.option( + '--json-output', + is_flag=True, + help=helptexts.JSON_OUTPUT) + + self.dry_execution = click.option( + '--dry', + is_flag=True, + help=helptexts.DRY_EXECUTION) + + self.reset_config = click.option( + '--reset-config', + is_flag=True, + help=helptexts.RESET_CONFIG) + + self.descending = click.option( + '--descending', + required=False, + is_flag=True, + default=False, + help=helptexts.DESCENDING) + + self.service_template_filename = click.option( + '-n', + '--service-template-filename', + default=DEFAULT_SERVICE_TEMPLATE_FILENAME, + help=helptexts.SERVICE_TEMPLATE_FILENAME) + + @staticmethod + def verbose(expose_value=False): + return click.option( + '-v', + '--verbose', + count=True, + callback=set_verbosity_level, + expose_value=expose_value, + is_eager=True, + help=helptexts.VERBOSE) + + @staticmethod + def inputs(help): + return click.option( + '-i', + '--inputs', + multiple=True, + callback=inputs_callback, + help=help) + + @staticmethod + def force(help): + return click.option( + '-f', + '--force', + is_flag=True, + help=help) + + @staticmethod + def task_max_attempts(default=1): + return click.option( + '--task-max-attempts', + type=int, + default=default, + help=helptexts.TASK_MAX_ATTEMPTS.format(default)) + + @staticmethod + def sort_by(default='created_at'): + return click.option( + '--sort-by', + required=False, + default=default, + help=helptexts.SORT_BY) + + @staticmethod + def task_retry_interval(default=1): + return click.option( + '--task-retry-interval', + type=int, + default=default, + help=helptexts.TASK_RETRY_INTERVAL.format(default)) + + @staticmethod + def service_id(required=False): + return click.option( + '-s', + '--service-id', + required=required, + help=helptexts.SERVICE_ID) + + @staticmethod + def execution_id(required=False): + return click.option( + '-e', + '--execution-id', + required=required, + help=helptexts.EXECUTION_ID) + + @staticmethod + def service_template_id(required=False): + return click.option( + '-t', + '--service-template-id', + required=required, + help=helptexts.SERVICE_TEMPLATE_ID) + + @staticmethod + def service_template_path(required=False): + return click.option( + '-p', + '--service-template-path', + required=required, + type=click.Path(exists=True)) + + @staticmethod + def service_name(required=False): + return click.option( + '-s', + '--service-name', + required=required, + help=helptexts.SERVICE_ID) + + @staticmethod + def service_template_name(required=False): + return click.option( + '-t', + '--service-template-name', + required=required, + help=helptexts.SERVICE_ID) + + +options = Options() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/helptexts.py ---------------------------------------------------------------------- diff --git a/aria/cli/helptexts.py b/aria/cli/helptexts.py new file mode 100644 index 0000000..6e31f47 --- /dev/null +++ b/aria/cli/helptexts.py @@ -0,0 +1,49 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +DEFAULT_MUTUALITY_MESSAGE = 'Cannot be used simultaneously' +VERBOSE = \ + "Show verbose output. You can supply this up to three times (i.e. -vvv)" + +VERSION = "Display the version and exit" +FORCE_RESET = "Confirmation for resetting ARIA's working directory" +RESET_CONFIG = "Reset ARIA's user configuration" + +SERVICE_TEMPLATE_ID = "The unique identifier for the service template" +SERVICE_ID = "The unique identifier for the service" +EXECUTION_ID = "The unique identifier for the execution" + +SERVICE_TEMPLATE_PATH = "The path to the application's service template file" +SERVICE_TEMPLATE_FILENAME = ( + "The name of the archive's main service template file. " + "This is only relevant if uploading a non-csar archive") +INPUTS_PARAMS_USAGE = ( + '(Can be provided as wildcard based paths ' + '(*.yaml, /my_inputs/, etc..) to YAML files, a JSON string or as ' + 'key1=value1;key2=value2). This argument can be used multiple times') +SERVICE_INPUTS = "Inputs for the service {0}".format(INPUTS_PARAMS_USAGE) +EXECUTION_INPUTS = "Inputs for the execution {0}".format(INPUTS_PARAMS_USAGE) + +TASK_RETRY_INTERVAL = \ + "How long of a minimal interval should occur between task retry attempts [default: {0}]" +TASK_MAX_ATTEMPTS = \ + "How many times should a task be attempted in case of failures [default: {0}]" +DRY_EXECUTION = "Execute a workflow dry run (prints operations information without causing side " \ + "effects)" +IGNORE_AVAILABLE_NODES = "Delete the service even if it has available nodes" +SORT_BY = "Key for sorting the list" +DESCENDING = "Sort list in descending order [default: False]" +JSON_OUTPUT = "Output logs in a consumable JSON format" http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/79f5d78e/aria/cli/main.py ---------------------------------------------------------------------- diff --git a/aria/cli/main.py b/aria/cli/main.py index 01d224c..02cf095 100644 --- a/aria/cli/main.py +++ b/aria/cli/main.py @@ -15,7 +15,7 @@ from aria import install_aria_extensions from aria.cli import commands -from aria.cli.cli import aria +from aria.cli.core import aria @aria.group(name='aria')
