Skip to content

Commit

Permalink
Merge pull request Azure#19 from Azure/master
Browse files Browse the repository at this point in the history
Sync with the official repo
  • Loading branch information
ShichaoQiu committed Aug 25, 2020
2 parents 5ddc1ef + ddec845 commit db582a5
Show file tree
Hide file tree
Showing 110 changed files with 16,856 additions and 3,280 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,5 @@
/src/attestation/ @YalinLi0312 @bim-msft

/src/guestconfig/ @gehuan

/src/swiftlet/ @qwordy
19 changes: 18 additions & 1 deletion src/ai-did-you-mean-this/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,21 @@ Release History

0.2.0
+++++
* Change name of required service parameter
* Change name of required service parameter

0.3.0
+++++
* Log custom telemetry data when given permission by the user to do so.

* Record exceptions thrown by the extension.
* Track various performance and health metrics.
* Track what suggestions are shown.

* Fix incorrect parsing of argument placeholders.
* Support parameter prefix matching for feature parity with the CLI parser.
* Add preliminary support for partial command matching

* Fixes bug where certain command groups are not recognized.

* Improve handling of extension debug logs.
* Store extension version in a centralized location to improve maintainability.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azext_ai_did_you_mean_this.arguments import Arguments
from azext_ai_did_you_mean_this._types import ArgumentsType


class CliCommand():
parameters = Arguments('parameters', delim=',')
arguments = Arguments('arguments', delim='♠')

def __init__(self, command: str, parameters: ArgumentsType = '', arguments: ArgumentsType = ''):
self.command_only = parameters == '' and arguments == ''
self.command = command
self.parameters = parameters
self.arguments = arguments

arguments_len = len(self.arguments)
parameters_len = len(self.parameters)
if arguments_len < parameters_len:
missing_argument_count = parameters_len - arguments_len
for _ in range(missing_argument_count):
self.arguments.append('')
elif arguments_len > parameters_len:
raise ValueError(f'Got more arguments ({arguments_len}) than parameters ({parameters_len}).')

def __str__(self):
buffer = []

if not self.command_only:
for (param, arg) in zip(self.parameters, self.arguments):
if not buffer:
buffer.append('')
if arg:
buffer.append(' '.join((param, arg)))
else:
buffer.append(param)

return f"{self.command}{' '.join(buffer)}"

def __eq__(self, value):
return (self.command == value.command and
self.parameters == value.parameters and
self.arguments == value.arguments)

def __hash__(self):
return hash((self.command, self.parameters, self.arguments))
109 changes: 109 additions & 0 deletions src/ai-did-you-mean-this/azext_ai_did_you_mean_this/_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from typing import List, Tuple, Union

from azure.cli.core.commands import AzCliCommand

from azext_ai_did_you_mean_this._logging import get_logger
from azext_ai_did_you_mean_this._parameter import (GLOBAL_PARAM_BLOCKLIST,
GLOBAL_PARAM_LOOKUP_TBL,
Parameter, parameter_gen)
from azext_ai_did_you_mean_this._types import ParameterTableType

logger = get_logger(__name__)


class Command():

def __init__(self, command: str, parameters: List[Parameter]):
self.command: str = command
self.parameters = parameters
self.parameter_lookup_table = {}
self.parameter_lookup_table.update(GLOBAL_PARAM_LOOKUP_TBL)

for parameter in self.parameters:
self.parameter_lookup_table[parameter.standard_form] = None

for alias in parameter.aliases:
self.parameter_lookup_table[alias] = parameter.standard_form

@classmethod
def normalize(cls, command: Union[None, 'Command'], *parameters: Tuple[str]):
normalized_parameters = []
unrecognized_parameters = []
parameter_lookup_table = command.parameter_lookup_table if command else GLOBAL_PARAM_LOOKUP_TBL.copy()
terms = parameter_lookup_table.keys()

def is_recognized(parameter: str) -> bool:
return parameter in parameter_lookup_table

def match_prefix(parameter: str) -> str:
matches: List[str] = [term for term in terms if term.startswith(parameter)]
if len(matches) == 1:
return matches[0]

return parameter

def get_normalized_form(parameter) -> str:
normalized_form = None

if not is_recognized(parameter):
parameter = match_prefix(parameter)

normalized_form = parameter_lookup_table.get(parameter, None) or parameter
return normalized_form

for parameter in parameters:
normalized_form = get_normalized_form(parameter)

if normalized_form in GLOBAL_PARAM_BLOCKLIST:
continue
if is_recognized(normalized_form):
normalized_parameters.append(normalized_form)
else:
unrecognized_parameters.append(normalized_form)

return sorted(set(normalized_parameters)), sorted(set(unrecognized_parameters))

@classmethod
def get_parameter_table(cls, command_table: dict, command: str,
recurse: bool = True) -> Tuple[ParameterTableType, str]:
az_cli_command: Union[AzCliCommand, None] = command_table.get(command, None)
parameter_table: ParameterTableType = az_cli_command.arguments if az_cli_command else {}
partial_match = True

if not az_cli_command:
partial_match = any(cmd for cmd in command_table if cmd.startswith(command))

# if the specified command was not found and no similar command exists and recursive search is enabled...
if not az_cli_command and not partial_match and recurse:
# if there are at least two tokens separated by whitespace, remove the last token
last_delim_idx = command.rfind(' ')
if last_delim_idx != -1:
logger.debug('Removing unknown token "%s" from command.', command[last_delim_idx + 1:])
# try to find the truncated command.
parameter_table, command = cls.get_parameter_table(
command_table,
command[:last_delim_idx],
recurse=False
)

return parameter_table, command

@staticmethod
def parse(command_table: dict, command: str, recurse: bool = True) -> Tuple['Command', str]:
instance: 'Command' = None
(parameter_table, command) = Command.get_parameter_table(
command_table,
command,
recurse
)

if parameter_table:
parameters = [parameter for parameter in parameter_gen(parameter_table)]
instance = Command(command, parameters)

return instance, command
25 changes: 25 additions & 0 deletions src/ai-did-you-mean-this/azext_ai_did_you_mean_this/_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# time to wait for connection to aladdin service in seconds.
SERVICE_CONNECTION_TIMEOUT = 10

EXTENSION_NAME = 'ai-did-you-mean-this'

EXTENSION_NICKNAME = 'Thoth'

THOTH_LOG_PREFIX = f'[{EXTENSION_NICKNAME}]'

UNEXPECTED_ERROR_STR = (
'An unexpected error occurred.'
)

UPDATE_RECOMMENDATION_STR = (
"Better failure recovery recommendations are available from the latest version of the CLI. "
"Please update for the best experience.\n"
Expand Down Expand Up @@ -36,3 +49,15 @@
UNABLE_TO_CALL_SERVICE_STR = (
'Either the subscription ID or correlation ID was not set. Aborting operation.'
)

RECOMMEND_RECOVERY_OPTIONS_LOG_FMT_STR = (
'recommend_recovery_options: version: "%s", command: "%s", parameters: "%s", extension: "%s"'
)

CALL_ALADDIN_SERVICE_LOG_FMT_STR = (
'call_aladdin_service: version: "%s", command: "%s", parameters: "%s"'
)

RECOMMENDATION_PROCESSING_TIME_FMT_STR = (
'The overall time it took to process failure recovery recommendations was %.2fms.'
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

helps['ai-did-you-mean-this'] = """
type: group
short-summary: Add recommendations for recovering from failure.
short-summary: Automatically adds failure recovery suggestions for supported scenarios.
"""

helps['ai-did-you-mean-this version'] = """
Expand Down
28 changes: 28 additions & 0 deletions src/ai-did-you-mean-this/azext_ai_did_you_mean_this/_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import logging

from knack.log import get_logger as get_knack_logger

from azext_ai_did_you_mean_this._const import THOTH_LOG_PREFIX

LOG_PREFIX_KEY = 'extension_log_prefix'


class ExtensionLoggerAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
buffer = [msg]

if LOG_PREFIX_KEY in self.extra:
buffer.insert(0, f'{self.extra[LOG_PREFIX_KEY]}:')

return ' '.join(buffer), kwargs


def get_logger(module_name: str) -> logging.LoggerAdapter:
logger = get_knack_logger(module_name)
adapter = ExtensionLoggerAdapter(logger, {LOG_PREFIX_KEY: THOTH_LOG_PREFIX})
return adapter
89 changes: 89 additions & 0 deletions src/ai-did-you-mean-this/azext_ai_did_you_mean_this/_parameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from collections import defaultdict
from typing import Iterator, List

from azext_ai_did_you_mean_this._logging import get_logger
from azext_ai_did_you_mean_this._types import (ParameterTableType, OptionListType)

logger = get_logger(__name__)

GLOBAL_PARAM_SHORTHAND_LOOKUP_TBL = {
'-h': '--help',
'-o': '--output',
}

GLOBAL_PARAM_LOOKUP_TBL = {
**GLOBAL_PARAM_SHORTHAND_LOOKUP_TBL,
'--only-show-errors': None,
'--help': None,
'--output': None,
'--query': None,
'--debug': None,
'--verbose': None
}

GLOBAL_PARAM_BLOCKLIST = {
'--only-show-errors',
'--help',
'--debug',
'--verbose',
}

SUPPRESSED_STR = "==SUPPRESS=="


def has_len_op(value):
return hasattr(value, '__len__') and callable(value.__len__)


class Parameter():

DEFAULT_STATE = defaultdict(
None,
options_list=[],
choices=[],
required=False,
)

def __init__(self, alias: str, **kwargs):
self._options = None

self.state = defaultdict(None, Parameter.DEFAULT_STATE)
self.state.update(**kwargs)
self.alias = alias

self.options = self.state.get('options_list', [])

sorted_options = sorted(self.options, key=len, reverse=True)
self.standard_form = next(iter(sorted_options), None)
self.aliases = set(self.options) - set((self.standard_form,))

@property
def configurable(self) -> bool:
return self.state['configured_default'] is not None

@property
def suppressed(self) -> bool:
return self.state['help'] == SUPPRESSED_STR

@property
def options(self) -> List[str]:
return self._options

@options.setter
def options(self, option_list: OptionListType):
self._options = [option for option in option_list if has_len_op(option)]


def parameter_gen(parameter_table: ParameterTableType) -> Iterator[Parameter]:
for alias, argument in parameter_table.items():
parameter = Parameter(alias, **argument.type.settings)

if not parameter.suppressed:
yield parameter
else:
logger.debug('Discarding supressed parameter "%s"', alias)
Loading

0 comments on commit db582a5

Please sign in to comment.