Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/azure-cli-core/azure/cli/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

# [Reserved, in case of future usage]
# Modules that will always be loaded. They don't expose commands but hook into CLI core.
ALWAYS_LOADED_MODULES = []
ALWAYS_LOADED_MODULES = ['hint']
# Extensions that will always be loaded if installed. They don't expose commands but hook into CLI core.
ALWAYS_LOADED_EXTENSIONS = ['azext_ai_examples', 'azext_next']

Expand Down
3 changes: 2 additions & 1 deletion src/azure-cli-core/azure/cli/core/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,8 @@ def execute(self, args):
return CommandResultItem(
event_data['result'],
table_transformer=self.commands_loader.command_table[parsed_args.command].table_transformer,
is_query_active=self.data['query_active'])
is_query_active=self.data['query_active'],
raw_result=results)

@staticmethod
def _extract_parameter_names(args):
Expand Down
10 changes: 10 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,16 @@ def test_get_parent_proc_name(self, mock_process_type):
parent2.name.return_value = "bash"
self.assertEqual(_get_parent_proc_name(), "pwsh")

def test_roughly_parse_command(self):
from azure.cli.core.util import roughly_parse_command
import shlex
self.assertEqual(roughly_parse_command(['login']), 'login')
self.assertEqual(roughly_parse_command(shlex.split('login --service-principal')), 'login')
self.assertEqual(roughly_parse_command(shlex.split('storage account --resource-group')), 'storage account')
self.assertEqual(roughly_parse_command(shlex.split('storage account -g')), 'storage account')
# Positional argument can't be distinguished
self.assertEqual(roughly_parse_command(shlex.split('config set output=table')), 'config set output=table')


class TestBase64ToHex(unittest.TestCase):

Expand Down
46 changes: 46 additions & 0 deletions src/azure-cli/azure/cli/command_modules/hint/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.command_modules.hint.custom import login_hinter, demo_hint_hinter
from azure.cli.core import AzCommandsLoader

hinters = {
# The hinters are matched based on the order they appear in the dict
# https://docs.python.org/3/library/stdtypes.html#dict
# Changed in version 3.7: Dictionary order is guaranteed to be insertion order.
# This behavior was an implementation detail of CPython from 3.6.
'login': login_hinter,
'demo hint': demo_hint_hinter
}


class HintCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
from azure.cli.core.commands import CliCommandType
hint_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.hint.custom#{}')
super(HintCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=hint_custom)

def load_command_table(self, args):
self._register_hinters(args)
return self.command_table

def _register_hinters(self, args):
import re
from knack.events import EVENT_CLI_SUCCESSFUL_EXECUTE
from knack.log import get_logger
logger = get_logger(__name__)

from azure.cli.core.util import roughly_parse_command
command_line = roughly_parse_command(args)
for command_regex, hinter in hinters.items():
if re.fullmatch(command_regex, command_line):
logger.debug("Registering hinter: %s: %s", command_regex, hinter.__name__)
self.cli_ctx.register_event(EVENT_CLI_SUCCESSFUL_EXECUTE, hinter)
# Improve if more than one hinters are needed
break


COMMAND_LOADER_CLS = HintCommandsLoader
103 changes: 103 additions & 0 deletions src/azure-cli/azure/cli/command_modules/hint/custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.style import print_styled_text, Style
from azure.cli.core.decorators import suppress_all_exceptions


def _get_default_account_text(accounts):
"""Return the type (tenant or subscription) and display text for the default account.

- For tenant account, only show the tenant ID.
- For subscription account, if name can uniquely identify the account, only show the name;
Otherwise, show both name and ID.
"""
account = next(s for s in accounts if s['isDefault'] is True)
account_name = account['name']
account_id = account['id']

# Tenant account
from azure.cli.core._profile import _TENANT_LEVEL_ACCOUNT_NAME
if account_name == _TENANT_LEVEL_ACCOUNT_NAME:
return "tenant", account_id

# Subscription account
# Check if name can uniquely identity the subscription
accounts_with_name = [a for a in accounts if a['name'] == account_name]
if len(accounts_with_name) == 1:
# For unique name, only show the name
account_text = account_name
else:
# If more than 1 accounts have the same name, also show ID
account_text = '{} ({})'.format(account_name, account_id)

return 'subscription', account_text


@suppress_all_exceptions()
def login_hinter(cli_ctx, result): # pylint: disable=unused-argument
account_type, account_text = _get_default_account_text(result.raw_result)

command_placeholder = '{:44s}'
selected_sub = [
(Style.PRIMARY, 'Your default {} is '.format(account_type)),
(Style.IMPORTANT, account_text),
]
print_styled_text(selected_sub)
print_styled_text()

# TRY
try_commands = [
(Style.PRIMARY, 'TRY\n'),
(Style.PRIMARY, command_placeholder.format('az upgrade')),
(Style.SECONDARY, 'Upgrade to the latest CLI version in tool\n'),
(Style.PRIMARY, command_placeholder.format('az account set --subscription <name or id>')),
(Style.SECONDARY, 'Set your default subscription account\n'),
(Style.PRIMARY, command_placeholder.format('az config set output=table')),
(Style.SECONDARY, 'Set your default output to be in table format\n')
]
print_styled_text(try_commands)


@suppress_all_exceptions()
def demo_hint_hinter(cli_ctx, result): # pylint: disable=unused-argument
result = result.raw_result
key_placeholder = '{:>25s}' # right alignment, 25 width
command_placeholder = '{:40s}'
projection = [
(Style.PRIMARY, 'The hinter can parse the output to show a "projection" of the output, like\n\n'),
(Style.PRIMARY, key_placeholder.format('Subscription name: ')),
(Style.IMPORTANT, result['name']),
(Style.PRIMARY, '\n'),
(Style.PRIMARY, key_placeholder.format('Subscription ID: ')),
(Style.IMPORTANT, result['id']),
(Style.PRIMARY, '\n'),
(Style.PRIMARY, key_placeholder.format('User: ')),
(Style.IMPORTANT, result['user']['name']),
]
print_styled_text(projection)
print_styled_text()

# TRY
try_commands = [
(Style.PRIMARY, 'TRY\n'),
(Style.PRIMARY, command_placeholder.format('az upgrade')),
(Style.SECONDARY, 'Upgrade to the latest CLI version in tool\n'),
(Style.PRIMARY, command_placeholder.format('az account set -s <sub_id or sub_name>')),
(Style.SECONDARY, 'Set your default subscription account\n'),
(Style.PRIMARY, command_placeholder.format('az config set output=table')),
(Style.SECONDARY, 'Set your default output to be in table format\n'),
(Style.PRIMARY, command_placeholder.format('az feedback')),
(Style.SECONDARY, 'File us your latest issue encountered\n'),
(Style.PRIMARY, command_placeholder.format('az next')),
(Style.SECONDARY, 'Get some ideas on next steps\n'),
]
print_styled_text(try_commands)

hyperlink = [
(Style.PRIMARY, 'You may also show a hyperlink for more detail: '),
(Style.HYPERLINK, 'https://docs.microsoft.com/cli/azure/'),
]
print_styled_text(hyperlink)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import unittest
import mock

from azure.cli.command_modules.hint.custom import _get_default_account_text


class HintTest(unittest.TestCase):

def test_get_default_account_text(self):
test_accounts = [
# Subscription with unique name
{
"cloudName": "AzureCloud",
"homeTenantId": "ca97aaa0-5a12-4ae3-8929-c8fb57dd93d6",
"id": "2b8e6bbc-631a-4bf6-b0c6-d4947b3c79dd",
"isDefault": False,
"managedByTenants": [],
"name": "Unique Name",
"state": "Enabled",
"tenantId": "ca97aaa0-5a12-4ae3-8929-c8fb57dd93d6",
"user": {
"name": "[email protected]",
"type": "user"
}
},
# Subscription with duplicated name
{
"cloudName": "AzureCloud",
"homeTenantId": "ca97aaa0-5a12-4ae3-8929-c8fb57dd93d6",
"id": "414af076-009b-4282-9a0a-acf75bcb037e",
"isDefault": False,
"managedByTenants": [],
"name": "Duplicated Name",
"state": "Enabled",
"tenantId": "ca97aaa0-5a12-4ae3-8929-c8fb57dd93d6",
"user": {
"name": "[email protected]",
"type": "user"
}
},
# Subscription with duplicated name
{
"cloudName": "AzureCloud",
"homeTenantId": "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a",
"id": "0b1f6471-1bf0-4dda-aec3-cb9272f09590",
"isDefault": False,
"managedByTenants": [],
"name": "Duplicated Name",
"state": "Enabled",
"tenantId": "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a",
"user": {
"name": "[email protected]",
"type": "user"
}
},
# Tenant
{
"cloudName": "AzureCloud",
"id": "246b1785-9030-40d8-a0f0-d94b15dc002c",
"isDefault": False,
"name": "N/A(tenant level account)",
"state": "Enabled",
"tenantId": "246b1785-9030-40d8-a0f0-d94b15dc002c",
"user": {
"name": "[email protected]",
"type": "user"
}
}
]
expected_result = [
("subscription", "Unique Name"),
("subscription", "Duplicated Name (414af076-009b-4282-9a0a-acf75bcb037e)"),
("subscription", "Duplicated Name (0b1f6471-1bf0-4dda-aec3-cb9272f09590)"),
("tenant", "246b1785-9030-40d8-a0f0-d94b15dc002c")
]

for i in range(len(test_accounts)):
# Mark each account as default and check the result
with mock.patch.dict(test_accounts[i], {'isDefault': True}):
self.assertEqual(_get_default_account_text(test_accounts), expected_result[i])
5 changes: 5 additions & 0 deletions src/azure-cli/azure/cli/command_modules/util/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@
type: command
short-summary: A demo showing supported text styles.
"""

helps['demo hint'] = """
type: command
short-summary: A demo showing post-output hint.
"""
1 change: 1 addition & 0 deletions src/azure-cli/azure/cli/command_modules/util/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ def load_command_table(self, _):

with self.command_group('demo', deprecate_info=g.deprecate(hide=True)) as g:
g.custom_command('style', 'demo_style')
g.custom_command('hint', 'demo_hint')
22 changes: 22 additions & 0 deletions src/azure-cli/azure/cli/command_modules/util/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,25 @@ def demo_style(cmd, theme=None): # pylint: disable=unused-argument
(Style.WARNING, "WARNING: The subscription has been disabled!")
]
print_styled_text(styled_text)


def demo_hint(cmd): # pylint: disable=unused-argument
test_dict = {
"cloudName": "AzureCloud",
"homeTenantId": "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a",
"id": "0b1f6471-1bf0-4dda-aec3-cb9272f09590",
"isDefault": True,
"managedByTenants": [
{
"tenantId": "2f4a9838-26b7-47ee-be60-ccc1fdec5953"
}
],
"name": "AzureSDKTest",
"state": "Enabled",
"tenantId": "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a",
"user": {
"name": "[email protected]",
"type": "user"
}
}
return test_dict