From b4234e8eef766bd8fa0d9a2fffbfa86662f079e9 Mon Sep 17 00:00:00 2001 From: kai ru Date: Thu, 12 Sep 2024 10:42:13 +0800 Subject: [PATCH 1/5] add command aaz-dev cli generate-powershell --- src/aaz_dev/cli/api/__init__.py | 3 +- src/aaz_dev/cli/api/_cmds.py | 48 +++++++++++++++++++ src/aaz_dev/cli/api/ps.py | 14 ++++++ .../cli/controller/ps_config_generator.py | 6 +++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/aaz_dev/cli/api/ps.py create mode 100644 src/aaz_dev/cli/controller/ps_config_generator.py diff --git a/src/aaz_dev/cli/api/__init__.py b/src/aaz_dev/cli/api/__init__.py index b8205959..843abb09 100644 --- a/src/aaz_dev/cli/api/__init__.py +++ b/src/aaz_dev/cli/api/__init__.py @@ -1,6 +1,7 @@ def register_blueprints(app): - from . import az, portal, _cmds + from . import az, ps, portal, _cmds app.register_blueprint(_cmds.bp) app.register_blueprint(az.bp) + app.register_blueprint(ps.bp) app.register_blueprint(portal.bp) diff --git a/src/aaz_dev/cli/api/_cmds.py b/src/aaz_dev/cli/api/_cmds.py index ea647a41..080931bc 100644 --- a/src/aaz_dev/cli/api/_cmds.py +++ b/src/aaz_dev/cli/api/_cmds.py @@ -237,3 +237,51 @@ def _build_profile(profile_name, commands_map): group_names = parent_group_names return profile + + +@bp.cli.command("generate-powershell", short_help="Generate powershell code based on selected azure cli module.") +@click.option( + "--aaz-path", '-a', + type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True), + default=Config.AAZ_PATH, + required=not Config.AAZ_PATH, + callback=Config.validate_and_setup_aaz_path, + expose_value=False, + help="The local path of aaz repo." +) +@click.option( + "--cli-path", '-c', + type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True), + callback=Config.validate_and_setup_cli_path, + help="The local path of azure-cli repo. Only required when generate code to azure-cli repo." +) +@click.option( + "--cli-extension-path", '-e', + type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True), + callback=Config.validate_and_setup_cli_extension_path, + help="The local path of azure-cli-extension repo. Only required when generate code to azure-cli-extension repo." +) +@click.option( + "--extension-or-module-name", '--name', + required=True, + help="Name of the module in azure-cli or the extension in azure-cli-extensions" +) +@click.option( + "--swagger-module-path", "--sm", + type=click.Path(file_okay=False, dir_okay=True, readable=True, resolve_path=True), + default=Config.SWAGGER_MODULE_PATH, + required=not Config.SWAGGER_MODULE_PATH, + callback=Config.validate_and_setup_swagger_module_path, + expose_value=False, + help="The local path of swagger module." +) +@click.option( + "--resource-provider", "--rp", + default=Config.DEFAULT_RESOURCE_PROVIDER, + required=not Config.DEFAULT_RESOURCE_PROVIDER, + callback=Config.validate_and_setup_default_resource_provider, + expose_value=False, + help="The resource provider name." +) +def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_path=None): + pass diff --git a/src/aaz_dev/cli/api/ps.py b/src/aaz_dev/cli/api/ps.py new file mode 100644 index 00000000..d2cc8858 --- /dev/null +++ b/src/aaz_dev/cli/api/ps.py @@ -0,0 +1,14 @@ +from flask import Blueprint, jsonify, request, url_for + +from utils.config import Config +from utils import exceptions +from cli.controller.az_module_manager import AzMainManager, AzExtensionManager +from cli.controller.portal_cli_generator import PortalCliGenerator +from cli.model.view import CLIModule +from command.controller.specs_manager import AAZSpecsManager +import logging + +logging.basicConfig(level="INFO") + +bp = Blueprint('ps', __name__, url_prefix='/CLI/PS') + diff --git a/src/aaz_dev/cli/controller/ps_config_generator.py b/src/aaz_dev/cli/controller/ps_config_generator.py new file mode 100644 index 00000000..a368bd49 --- /dev/null +++ b/src/aaz_dev/cli/controller/ps_config_generator.py @@ -0,0 +1,6 @@ +from .az_module_manager import AzModuleManager + +class PSAutoRestConfigurationGenerator: + + def __init__(self, az_module_manager: AzModuleManager, module_name) -> None: + az_module_manager.load_module() From d2510d23dd02830ecd2449d292657e09c5894a79 Mon Sep 17 00:00:00 2001 From: kai ru Date: Fri, 13 Sep 2024 17:05:33 +0800 Subject: [PATCH 2/5] save --- src/aaz_dev/cli/api/_cmds.py | 40 +- .../cli/controller/ps_config_generator.py | 184 +++++++++- src/aaz_dev/cli/templates/__init__.py | 3 + .../powershell/configuration.yaml.j2 | 24 ++ src/aaz_dev/ps_profile.json | 346 ++++++++++++++++++ .../swagger/model/specs/_resource_provider.py | 23 ++ 6 files changed, 603 insertions(+), 17 deletions(-) create mode 100644 src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 create mode 100644 src/aaz_dev/ps_profile.json diff --git a/src/aaz_dev/cli/api/_cmds.py b/src/aaz_dev/cli/api/_cmds.py index 080931bc..e4170900 100644 --- a/src/aaz_dev/cli/api/_cmds.py +++ b/src/aaz_dev/cli/api/_cmds.py @@ -253,13 +253,13 @@ def _build_profile(profile_name, commands_map): "--cli-path", '-c', type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True), callback=Config.validate_and_setup_cli_path, - help="The local path of azure-cli repo. Only required when generate code to azure-cli repo." + help="The local path of azure-cli repo. Only required when generate from azure-cli module." ) @click.option( "--cli-extension-path", '-e', type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True), callback=Config.validate_and_setup_cli_extension_path, - help="The local path of azure-cli-extension repo. Only required when generate code to azure-cli-extension repo." + help="The local path of azure-cli-extension repo. Only required when generate from azure-cli extension." ) @click.option( "--extension-or-module-name", '--name', @@ -267,21 +267,29 @@ def _build_profile(profile_name, commands_map): help="Name of the module in azure-cli or the extension in azure-cli-extensions" ) @click.option( - "--swagger-module-path", "--sm", + "--swagger-path", '-s', type=click.Path(file_okay=False, dir_okay=True, readable=True, resolve_path=True), - default=Config.SWAGGER_MODULE_PATH, - required=not Config.SWAGGER_MODULE_PATH, - callback=Config.validate_and_setup_swagger_module_path, - expose_value=False, - help="The local path of swagger module." -) -@click.option( - "--resource-provider", "--rp", - default=Config.DEFAULT_RESOURCE_PROVIDER, - required=not Config.DEFAULT_RESOURCE_PROVIDER, - callback=Config.validate_and_setup_default_resource_provider, + default=Config.SWAGGER_PATH, + required=not Config.SWAGGER_PATH, + callback=Config.validate_and_setup_swagger_path, expose_value=False, - help="The resource provider name." + help="The local path of azure-rest-api-specs repo. Official repo is https://github.com/Azure/azure-rest-api-specs" ) def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_path=None): - pass + from cli.controller.ps_config_generator import PSAutoRestConfigurationGenerator + from cli.controller.az_module_manager import AzMainManager, AzExtensionManager + + if cli_path is not None: + assert Config.CLI_PATH is not None + manager = AzMainManager() + else: + assert cli_extension_path is not None + assert Config.CLI_EXTENSION_PATH is not None + manager = AzExtensionManager() + + if not manager.has_module(extension_or_module_name): + logger.error(f"Cannot find module or extension `{extension_or_module_name}`") + sys.exit(1) + + ps_generator = PSAutoRestConfigurationGenerator(manager, extension_or_module_name) + ps_generator.generate_config() diff --git a/src/aaz_dev/cli/controller/ps_config_generator.py b/src/aaz_dev/cli/controller/ps_config_generator.py index a368bd49..ef8c1b8d 100644 --- a/src/aaz_dev/cli/controller/ps_config_generator.py +++ b/src/aaz_dev/cli/controller/ps_config_generator.py @@ -1,6 +1,188 @@ from .az_module_manager import AzModuleManager +from swagger.controller.specs_manager import SwaggerSpecsManager +from command.controller.specs_manager import AAZSpecsManager +from utils.config import Config +from utils.exceptions import ResourceNotFind +from command.model.configuration import CMDConfiguration, CMDResource, CMDHttpOperation +import json +import logging +import inflect +import re +from swagger.model.specs._utils import map_path_2_repo + +from fuzzywuzzy import fuzz + + +logger = logging.getLogger('backend') + class PSAutoRestConfigurationGenerator: + _CAMEL_CASE_PATTERN = re.compile(r"^([a-zA-Z][a-z0-9]+)(([A-Z][a-z0-9]*)+)$") + _inflect_engine = inflect.engine() def __init__(self, az_module_manager: AzModuleManager, module_name) -> None: - az_module_manager.load_module() + self.module_manager = az_module_manager + self.module_name = module_name + self.aaz_specs_manager = AAZSpecsManager() + self.swagger_specs_manager = SwaggerSpecsManager() + self._ps_profile = None + + def generate_config(self): + module = self.module_manager.load_module(self.module_name) + + cli_profile = {} + + for cli_command in self.iter_cli_commands(module.profiles[Config.CLI_DEFAULT_PROFILE]): + names = cli_command.names + version_name = cli_command.version + aaz_cmd = self.aaz_specs_manager.find_command(*names) + if not aaz_cmd: + raise ResourceNotFind("Command '{}' not exist in AAZ".format(' '.join(names))) + version = None + for v in (aaz_cmd.versions or []): + if v.name == version_name: + version = v + break + if not version: + raise ResourceNotFind("Version '{}' of command '{}' not exist in AAZ".format(version_name, ' '.join(names))) + resource = v.resources[0] + cfg: CMDConfiguration = self.aaz_specs_manager.load_resource_cfg_reader(resource.plane, resource.id, resource.version) + if not cfg: + raise ResourceNotFind("Resource Configuration '{}' not exist in AAZ".format(resource.id)) + for resource in cfg.resources: + tag = (resource.plane, '/'.join(resource.mod_names), resource.rp_name) + if tag not in cli_profile: + cli_profile[tag] = {} + if resource.id not in cli_profile[tag]: + cli_profile[tag][resource.id] = { + "path": resource.path, + "cfg": cfg, + "commands": [], + "subresources": [], + } + cli_profile[tag][resource.id]["commands"].append(cli_command.names) + if resource.subresource: + cli_profile[tag][resource.id]["subresources"].append(resource.subresource) + + # TODO: let LLM to choice the plane and rp_name later + if len(cli_profile.keys()) > 1: + raise ValueError("Only one plane module and rp_name is supported") + (plane, mod_names, rp_name) = list(cli_profile.keys())[0] + cli_resources = cli_profile[(plane, mod_names, rp_name)] + + module_manager = self.swagger_specs_manager.get_module_manager(plane, mod_names.split('/')) + rp = module_manager.get_openapi_resource_provider(rp_name) + swagger_resources = rp.get_resource_map_by_tag(rp.default_tag) + if not swagger_resources: + raise ResourceNotFind("Resources not find in Swagger") + + ps_profile = {} + + for resource_id, resource in swagger_resources.items(): + resource = list(resource.values())[0] + op_group_name = self.get_operation_group_name(resource) + if op_group_name not in ps_profile: + ps_profile[op_group_name] = { + "operations": {}, + } + if resource_id not in cli_resources: + for op_tag, method in resource.operations.items(): + ps_profile[op_group_name]["operations"][op_tag] = { + "tag": op_tag, + "delete": True, + "method": method, + } + else: + for cmd_names in cli_resources[resource_id]["commands"]: + cfg = cli_resources[resource_id]["cfg"] + command = cfg.find_command(*cmd_names) + assert command is not None + for cmd_op in command.operations: + if not isinstance(cmd_op, CMDHttpOperation): + continue + op_tag = cmd_op.operation_id + if op_tag not in resource.operations: + continue + method = resource.operations[op_tag] + ps_profile[op_group_name]["operations"][op_tag] = { + "delete": False, + "method": method, + } + for op_tag, method in resource.operations.items(): + if op_tag not in ps_profile[op_group_name]["operations"]: + ps_profile[op_group_name]["operations"][op_tag] = { + "delete": False if method == "patch" else True, # PowerShell Prefer Patch Method for update commands + "method": method, + } + + for group in ps_profile.values(): + delete_all = True + for op_tag, op in group["operations"].items(): + if not op["delete"]: + delete_all = False + break + group["delete"] = delete_all + + self._ps_profile = ps_profile + # # write to json file + # with open("ps_profile.json", 'w') as f: + # json.dump(ps_profile, f, indent=4) + + def iter_cli_commands(self, profile): + for command_group in profile.command_groups.values(): + for cli_command in self._iter_cli_commands(command_group): + yield cli_command + + def _iter_cli_commands(self, view_command_group): + if view_command_group.commands: + for cli_command in view_command_group.commands.values(): + yield cli_command + if view_command_group.command_groups: + for command_group in view_command_group.command_groups.values(): + for cli_command in self._iter_cli_commands(command_group): + yield cli_command + + def get_operation_group_name(self, resource): + + operation_groups = set() + for operation_id, method in resource.operations.items(): + op_group = self._parse_operation_group_name(resource, operation_id, method) + operation_groups.add(op_group) + + if None in operation_groups: + return None + + if len(operation_groups) == 1: + return operation_groups.pop() + + op_group_name = sorted( + operation_groups, + key=lambda nm: fuzz.partial_ratio(resource.id, nm), # use the name which is closest to resource_id + reverse=True + )[0] + return op_group_name + + def _parse_operation_group_name(self, resource, op_id, method): + # extract operation group name from operation_id + value = op_id.strip() + value = value.replace('-', '_') + if '_' in value: + parts = value.split('_') + op_group_name = parts[0] + if op_group_name.lower() in ("create", "get", "update", "delete", "patch"): + op_group_name = parts[1] + else: + if ' ' in value: + value = value.replace(' ', '') # Changed to Camel Case + match = self._CAMEL_CASE_PATTERN.match(value) + if not match: + logger.error(f"InvalidOperationIdFormat:" + f"\toperationId should be in format of '[OperationGroupName]_[OperationName]' " + f"or '[Verb][OperationGroupName]':\n" + f"\tfile: {map_path_2_repo(resource.file_path)}\n" + f"\tpath: {resource.path}\n" + f"\tmethod: {method} operationId: {op_id}\n") + return None + op_group_name = match[2] # [OperationGroupName] + + return self._inflect_engine.singular_noun(op_group_name) or op_group_name diff --git a/src/aaz_dev/cli/templates/__init__.py b/src/aaz_dev/cli/templates/__init__.py index 7eafd9b8..83bb8293 100644 --- a/src/aaz_dev/cli/templates/__init__.py +++ b/src/aaz_dev/cli/templates/__init__.py @@ -60,5 +60,8 @@ def get_templates(): } } }, + 'powershell': { + "configuration": env.get_template("powershell/configuration.yaml.j2") + }, } return _templates diff --git a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 new file mode 100644 index 00000000..8e9f8f1a --- /dev/null +++ b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 @@ -0,0 +1,24 @@ + + +directive: + # Remove the set-* cmdlet + - where: + verb: Set + remove: true + + # Remove variants + - where: + variant: ^(Create|Update)(?!.*?(Expanded|JsonFilePath|JsonString)) + remove: true + - where: + variant: ^CreateViaIdentity.*$ + remove: true + + # TODO: Remove variants with post verb + + # Remove APIs + + + # TODO: Rename parameter + + # TODO: Model complex objects diff --git a/src/aaz_dev/ps_profile.json b/src/aaz_dev/ps_profile.json new file mode 100644 index 00000000..3c5ba799 --- /dev/null +++ b/src/aaz_dev/ps_profile.json @@ -0,0 +1,346 @@ +{ + "AutonomousDatabase": { + "operations": { + "AutonomousDatabases_ListBySubscription": { + "delete": false, + "method": "get" + }, + "AutonomousDatabases_ListByResourceGroup": { + "delete": false, + "method": "get" + }, + "AutonomousDatabases_CreateOrUpdate": { + "delete": false, + "method": "put" + }, + "AutonomousDatabases_Delete": { + "delete": false, + "method": "delete" + }, + "AutonomousDatabases_Get": { + "delete": false, + "method": "get" + }, + "AutonomousDatabases_Update": { + "delete": false, + "method": "patch" + }, + "AutonomousDatabases_Failover": { + "delete": false, + "method": "post" + }, + "AutonomousDatabases_GenerateWallet": { + "delete": true, + "method": "post" + }, + "AutonomousDatabases_Restore": { + "delete": false, + "method": "post" + }, + "AutonomousDatabases_Shrink": { + "delete": true, + "method": "post" + }, + "AutonomousDatabases_Switchover": { + "delete": false, + "method": "post" + } + }, + "delete": false + }, + "CloudExadataInfrastructure": { + "operations": { + "CloudExadataInfrastructures_ListBySubscription": { + "delete": false, + "method": "get" + }, + "CloudExadataInfrastructures_ListByResourceGroup": { + "delete": false, + "method": "get" + }, + "CloudExadataInfrastructures_CreateOrUpdate": { + "delete": false, + "method": "put" + }, + "CloudExadataInfrastructures_Delete": { + "delete": false, + "method": "delete" + }, + "CloudExadataInfrastructures_Get": { + "delete": false, + "method": "get" + }, + "CloudExadataInfrastructures_Update": { + "delete": false, + "method": "patch" + }, + "CloudExadataInfrastructures_AddStorageCapacity": { + "delete": true, + "method": "post" + } + }, + "delete": false + }, + "CloudVmCluster": { + "operations": { + "CloudVmClusters_ListBySubscription": { + "delete": false, + "method": "get" + }, + "CloudVmClusters_ListByResourceGroup": { + "delete": false, + "method": "get" + }, + "CloudVmClusters_CreateOrUpdate": { + "delete": false, + "method": "put" + }, + "CloudVmClusters_Delete": { + "delete": false, + "method": "delete" + }, + "CloudVmClusters_Get": { + "delete": false, + "method": "get" + }, + "CloudVmClusters_Update": { + "delete": false, + "method": "patch" + }, + "CloudVmClusters_AddVms": { + "delete": false, + "method": "post" + }, + "CloudVmClusters_ListPrivateIpAddresses": { + "delete": true, + "method": "post" + }, + "CloudVmClusters_RemoveVms": { + "delete": false, + "method": "post" + } + }, + "delete": false + }, + "AutonomousDatabaseCharacterSet": { + "operations": { + "AutonomousDatabaseCharacterSets_ListByLocation": { + "delete": false, + "method": "get" + }, + "AutonomousDatabaseCharacterSets_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "AutonomousDatabaseNationalCharacterSet": { + "operations": { + "AutonomousDatabaseNationalCharacterSets_ListByLocation": { + "delete": false, + "method": "get" + }, + "AutonomousDatabaseNationalCharacterSets_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "AutonomousDatabaseVersion": { + "operations": { + "AutonomousDatabaseVersions_ListByLocation": { + "delete": false, + "method": "get" + }, + "AutonomousDatabaseVersions_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "DbSystemShape": { + "operations": { + "DbSystemShapes_ListByLocation": { + "delete": false, + "method": "get" + }, + "DbSystemShapes_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "DnsPrivateView": { + "operations": { + "DnsPrivateViews_ListByLocation": { + "delete": false, + "method": "get" + }, + "DnsPrivateViews_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "DnsPrivateZone": { + "operations": { + "DnsPrivateZones_ListByLocation": { + "delete": false, + "method": "get" + }, + "DnsPrivateZones_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "GiVersion": { + "operations": { + "GiVersions_ListByLocation": { + "delete": false, + "method": "get" + }, + "GiVersions_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "SystemVersion": { + "operations": { + "SystemVersions_ListByLocation": { + "delete": true, + "method": "get" + }, + "SystemVersions_Get": { + "delete": true, + "method": "get" + } + }, + "delete": true + }, + "OracleSubscription": { + "operations": { + "OracleSubscriptions_ListBySubscription": { + "delete": true, + "method": "get" + }, + "OracleSubscriptions_Get": { + "delete": true, + "method": "get" + }, + "OracleSubscriptions_CreateOrUpdate": { + "delete": true, + "method": "put" + }, + "OracleSubscriptions_Update": { + "delete": true, + "method": "patch" + }, + "OracleSubscriptions_Delete": { + "delete": true, + "method": "delete" + }, + "OracleSubscriptions_AddAzureSubscriptions": { + "delete": true, + "method": "post" + }, + "OracleSubscriptions_ListActivationLinks": { + "delete": true, + "method": "post" + }, + "OracleSubscriptions_ListCloudAccountDetails": { + "delete": true, + "method": "post" + }, + "OracleSubscriptions_ListSaasSubscriptionDetails": { + "delete": true, + "method": "post" + } + }, + "delete": true + }, + "AutonomousDatabaseBackup": { + "operations": { + "AutonomousDatabaseBackups_ListByAutonomousDatabase": { + "delete": false, + "method": "get" + }, + "AutonomousDatabaseBackups_CreateOrUpdate": { + "delete": false, + "method": "put" + }, + "AutonomousDatabaseBackups_Delete": { + "delete": false, + "method": "delete" + }, + "AutonomousDatabaseBackups_Get": { + "delete": false, + "method": "get" + }, + "AutonomousDatabaseBackups_Update": { + "delete": false, + "method": "patch" + } + }, + "delete": false + }, + "DbServer": { + "operations": { + "DbServers_ListByCloudExadataInfrastructure": { + "delete": false, + "method": "get" + }, + "DbServers_Get": { + "delete": true, + "method": "get" + } + }, + "delete": false + }, + "DbNode": { + "operations": { + "DbNodes_ListByCloudVmCluster": { + "delete": false, + "method": "get" + }, + "DbNodes_Get": { + "delete": true, + "method": "get" + }, + "DbNodes_Action": { + "delete": false, + "method": "post" + } + }, + "delete": false + }, + "VirtualNetworkAddress": { + "operations": { + "VirtualNetworkAddresses_ListByCloudVmCluster": { + "delete": true, + "method": "get" + }, + "VirtualNetworkAddresses_Get": { + "delete": true, + "method": "get" + }, + "VirtualNetworkAddresses_CreateOrUpdate": { + "delete": true, + "method": "put" + }, + "VirtualNetworkAddresses_Delete": { + "delete": true, + "method": "delete" + } + }, + "delete": true + } +} \ No newline at end of file diff --git a/src/aaz_dev/swagger/model/specs/_resource_provider.py b/src/aaz_dev/swagger/model/specs/_resource_provider.py index 20e85578..78f57f75 100644 --- a/src/aaz_dev/swagger/model/specs/_resource_provider.py +++ b/src/aaz_dev/swagger/model/specs/_resource_provider.py @@ -73,6 +73,29 @@ def get_resource_map_by_tag(self, tag): ): resource_map[resource.id][resource.version] = resource return resource_map + + @property + def default_tag(self): + if self._readme_path is None: + return None + + with open(self._readme_path, 'r', encoding='utf-8') as f: + readme = f.read() + lines = readme.split('\n') + for i in range(len(lines)): + line = lines[i] + if line.startswith('### Basic Information'): + lines = lines[i+1:] + break + latest_tag = None + for i in range(len(lines)): + line = lines[i] + if line.startswith('##'): + break + if line.startswith('tag:'): + latest_tag = line.split(':')[-1].strip() + break + return latest_tag @property def tags(self): From 8be560898dc297542f06ad38221e6327a4984e79 Mon Sep 17 00:00:00 2001 From: kai ru Date: Thu, 19 Sep 2024 15:52:33 +0800 Subject: [PATCH 3/5] Generated autorest configuration from cli --- requirements.txt | 2 +- src/aaz_dev/cli/api/_cmds.py | 8 +- .../cli/controller/ps_config_generator.py | 524 +++++++++- .../powershell/configuration.yaml.j2 | 43 +- .../model/configuration/_arg_builder.py | 10 +- .../command/model/configuration/_xml.py | 8 +- src/aaz_dev/ps_profile.json | 911 ++++++++++++++++-- .../swagger/controller/command_generator.py | 6 +- src/aaz_dev/swagger/model/specs/_resource.py | 12 +- 9 files changed, 1356 insertions(+), 168 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8fb281ca..15691c6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ schematics~=2.1.1 pyyaml~=6.0.2 fuzzywuzzy~=0.18.0 -inflect~=5.6.2 azure-mgmt-core~=1.3.0 +pluralizer~=1.2.0 lxml~=4.9.4 flask~=3.0.3 cachelib~=0.13.0 diff --git a/src/aaz_dev/cli/api/_cmds.py b/src/aaz_dev/cli/api/_cmds.py index e4170900..4eabb9f2 100644 --- a/src/aaz_dev/cli/api/_cmds.py +++ b/src/aaz_dev/cli/api/_cmds.py @@ -278,7 +278,7 @@ def _build_profile(profile_name, commands_map): def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_path=None): from cli.controller.ps_config_generator import PSAutoRestConfigurationGenerator from cli.controller.az_module_manager import AzMainManager, AzExtensionManager - + from cli.templates import get_templates if cli_path is not None: assert Config.CLI_PATH is not None manager = AzMainManager() @@ -292,4 +292,8 @@ def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_p sys.exit(1) ps_generator = PSAutoRestConfigurationGenerator(manager, extension_or_module_name) - ps_generator.generate_config() + ps_cfg = ps_generator.generate_config() + + tmpl = get_templates()['powershell']['configuration'] + data = tmpl.render(cfg=ps_cfg) + diff --git a/src/aaz_dev/cli/controller/ps_config_generator.py b/src/aaz_dev/cli/controller/ps_config_generator.py index ef8c1b8d..5def6f79 100644 --- a/src/aaz_dev/cli/controller/ps_config_generator.py +++ b/src/aaz_dev/cli/controller/ps_config_generator.py @@ -6,19 +6,30 @@ from command.model.configuration import CMDConfiguration, CMDResource, CMDHttpOperation import json import logging -import inflect +from pluralizer import Pluralizer import re +import os from swagger.model.specs._utils import map_path_2_repo from fuzzywuzzy import fuzz -logger = logging.getLogger('backend') +logger = logging.getLogger("backend") + +class PSAutoRestConfiguration: + + def __init__(self): + self.commit = None + self.version = None + self.module_name = None + self.readme_file = None + self.removed_subjects = [] + self.removed_verbs = [] class PSAutoRestConfigurationGenerator: _CAMEL_CASE_PATTERN = re.compile(r"^([a-zA-Z][a-z0-9]+)(([A-Z][a-z0-9]*)+)$") - _inflect_engine = inflect.engine() + _pluralizer = Pluralizer() def __init__(self, az_module_manager: AzModuleManager, module_name) -> None: self.module_manager = az_module_manager @@ -28,29 +39,45 @@ def __init__(self, az_module_manager: AzModuleManager, module_name) -> None: self._ps_profile = None def generate_config(self): + ps_cfg = PSAutoRestConfiguration() + # TODO: get the commit using git + ps_cfg.commit = "cbbe228fd422db02b65e2748f83df5f2bcad7581" + module = self.module_manager.load_module(self.module_name) cli_profile = {} - for cli_command in self.iter_cli_commands(module.profiles[Config.CLI_DEFAULT_PROFILE]): + for cli_command in self.iter_cli_commands( + module.profiles[Config.CLI_DEFAULT_PROFILE] + ): names = cli_command.names version_name = cli_command.version aaz_cmd = self.aaz_specs_manager.find_command(*names) if not aaz_cmd: - raise ResourceNotFind("Command '{}' not exist in AAZ".format(' '.join(names))) + raise ResourceNotFind( + "Command '{}' not exist in AAZ".format(" ".join(names)) + ) version = None - for v in (aaz_cmd.versions or []): + for v in aaz_cmd.versions or []: if v.name == version_name: version = v break if not version: - raise ResourceNotFind("Version '{}' of command '{}' not exist in AAZ".format(version_name, ' '.join(names))) + raise ResourceNotFind( + "Version '{}' of command '{}' not exist in AAZ".format( + version_name, " ".join(names) + ) + ) resource = v.resources[0] - cfg: CMDConfiguration = self.aaz_specs_manager.load_resource_cfg_reader(resource.plane, resource.id, resource.version) + cfg: CMDConfiguration = self.aaz_specs_manager.load_resource_cfg_reader( + resource.plane, resource.id, resource.version + ) if not cfg: - raise ResourceNotFind("Resource Configuration '{}' not exist in AAZ".format(resource.id)) + raise ResourceNotFind( + "Resource Configuration '{}' not exist in AAZ".format(resource.id) + ) for resource in cfg.resources: - tag = (resource.plane, '/'.join(resource.mod_names), resource.rp_name) + tag = (resource.plane, "/".join(resource.mod_names), resource.rp_name) if tag not in cli_profile: cli_profile[tag] = {} if resource.id not in cli_profile[tag]: @@ -62,36 +89,47 @@ def generate_config(self): } cli_profile[tag][resource.id]["commands"].append(cli_command.names) if resource.subresource: - cli_profile[tag][resource.id]["subresources"].append(resource.subresource) - + cli_profile[tag][resource.id]["subresources"].append( + resource.subresource + ) + # TODO: let LLM to choice the plane and rp_name later if len(cli_profile.keys()) > 1: raise ValueError("Only one plane module and rp_name is supported") (plane, mod_names, rp_name) = list(cli_profile.keys())[0] cli_resources = cli_profile[(plane, mod_names, rp_name)] - module_manager = self.swagger_specs_manager.get_module_manager(plane, mod_names.split('/')) + module_manager = self.swagger_specs_manager.get_module_manager( + plane, mod_names.split("/") + ) rp = module_manager.get_openapi_resource_provider(rp_name) swagger_resources = rp.get_resource_map_by_tag(rp.default_tag) if not swagger_resources: raise ResourceNotFind("Resources not find in Swagger") - ps_profile = {} + readme_parts= rp._readme_path.split(os.sep) + ps_cfg.readme_file = '/'.join(readme_parts[readme_parts.index("specification"):]) + ps_cfg.version = "0.1.0" + ps_cfg.module_name = mod_names.split("/")[0] + ps_profile = {} + + # create ps_profile by swagger resources for resource_id, resource in swagger_resources.items(): resource = list(resource.values())[0] + methods = set() + operations = {} op_group_name = self.get_operation_group_name(resource) - if op_group_name not in ps_profile: - ps_profile[op_group_name] = { - "operations": {}, - } if resource_id not in cli_resources: + # the whole resource id is not used in cli for op_tag, method in resource.operations.items(): - ps_profile[op_group_name]["operations"][op_tag] = { + operations[op_tag] = { "tag": op_tag, "delete": True, + "resource_id": resource_id, "method": method, } + methods.add(method) else: for cmd_names in cli_resources[resource_id]["commands"]: cfg = cli_resources[resource_id]["cfg"] @@ -102,31 +140,79 @@ def generate_config(self): continue op_tag = cmd_op.operation_id if op_tag not in resource.operations: + # make sure the operation is from the same resource continue method = resource.operations[op_tag] - ps_profile[op_group_name]["operations"][op_tag] = { + operations[op_tag] = { + "tag": op_tag, "delete": False, + "resource_id": resource_id, "method": method, } + methods.add(method) + for op_tag, method in resource.operations.items(): - if op_tag not in ps_profile[op_group_name]["operations"]: - ps_profile[op_group_name]["operations"][op_tag] = { - "delete": False if method == "patch" else True, # PowerShell Prefer Patch Method for update commands + if op_tag not in operations: + operations[op_tag] = { + "tag": op_tag, + "delete": ( + False if method == "patch" else True + ), # PowerShell Prefer Patch Method for update commands + "resource_id": resource_id, "method": method, } - - for group in ps_profile.values(): - delete_all = True - for op_tag, op in group["operations"].items(): - if not op["delete"]: - delete_all = False + methods.add(method) + + for op_tag, op in operations.items(): + variants = self.inferCommandNames(op_tag, op_group_name) + op['variants'] = [] + for variant in variants: + if op['method'] == 'put' and variant['action'] == 'Update': + if 'get' not in methods: + # "update" should be "set" if it's a PUT and not the generic update (GET+PUT) + variant['verb'] = 'Set' + else: + use_generic_update = True + for patch_op in operations.values(): + if patch_op['method'] == 'patch': + use_generic_update = patch_op['delete'] + break + if not use_generic_update: + continue + op['variants'].append(variant) + # make sure the variants should have the same subject + subjects = list(set([v['subject'] for v in op['variants']])) + assert len(subjects) == 1, f"Operation {op_tag} has different subjects: {subjects}" + + subject = subjects[0] + if subject not in ps_profile: + ps_profile[subject] = { + "operations": {}, + "delete": False, + } + ps_profile[subject]["operations"][op_tag] = op + + for subject in ps_profile.values(): + subject_delete = True + for op in subject['operations'].values(): + if not op['delete']: + subject_delete = False break - group["delete"] = delete_all + subject['delete'] = subject_delete self._ps_profile = ps_profile - # # write to json file - # with open("ps_profile.json", 'w') as f: - # json.dump(ps_profile, f, indent=4) + + ps_cfg.removed_subjects = [] + ps_cfg.removed_verbs = [] + for subject_name, subject in ps_profile.items(): + if subject['delete']: + ps_cfg.removed_subjects.append(subject_name) + continue + for op in subject['operations'].values(): + if op['delete']: + for variant in op['variants']: + ps_cfg.removed_verbs.append((variant['subject'], variant['verb'])) + return ps_cfg def iter_cli_commands(self, profile): for command_group in profile.command_groups.values(): @@ -143,7 +229,6 @@ def _iter_cli_commands(self, view_command_group): yield cli_command def get_operation_group_name(self, resource): - operation_groups = set() for operation_id, method in resource.operations.items(): op_group = self._parse_operation_group_name(resource, operation_id, method) @@ -157,32 +242,375 @@ def get_operation_group_name(self, resource): op_group_name = sorted( operation_groups, - key=lambda nm: fuzz.partial_ratio(resource.id, nm), # use the name which is closest to resource_id - reverse=True + key=lambda nm: fuzz.partial_ratio( + resource.id, nm + ), # use the name which is closest to resource_id + reverse=True, )[0] return op_group_name def _parse_operation_group_name(self, resource, op_id, method): # extract operation group name from operation_id value = op_id.strip() - value = value.replace('-', '_') - if '_' in value: - parts = value.split('_') + value = value.replace("-", "_") + if "_" in value: + parts = value.split("_") op_group_name = parts[0] if op_group_name.lower() in ("create", "get", "update", "delete", "patch"): op_group_name = parts[1] else: - if ' ' in value: - value = value.replace(' ', '') # Changed to Camel Case + if " " in value: + value = value.replace(" ", "") # Changed to Camel Case match = self._CAMEL_CASE_PATTERN.match(value) if not match: - logger.error(f"InvalidOperationIdFormat:" - f"\toperationId should be in format of '[OperationGroupName]_[OperationName]' " - f"or '[Verb][OperationGroupName]':\n" - f"\tfile: {map_path_2_repo(resource.file_path)}\n" - f"\tpath: {resource.path}\n" - f"\tmethod: {method} operationId: {op_id}\n") + logger.error( + f"InvalidOperationIdFormat:" + f"\toperationId should be in format of '[OperationGroupName]_[OperationName]' " + f"or '[Verb][OperationGroupName]':\n" + f"\tfile: {map_path_2_repo(resource.file_path)}\n" + f"\tpath: {resource.path}\n" + f"\tmethod: {method} operationId: {op_id}\n" + ) return None op_group_name = match[2] # [OperationGroupName] - return self._inflect_engine.singular_noun(op_group_name) or op_group_name + return self.singular_noun(op_group_name) + + @classmethod + def inferCommandNames(cls, operation_id, op_group_name): + parts = operation_id.split("_") + assert len(parts) == 2 + method = parts[1] + method = method[0].upper() + method[1:] + + if VERB_MAPPING.get(method): + return [ + cls.create_command_variant(method, [op_group_name], []) + ] + + # split camel case to words + words = re.findall(r'[A-Z][a-z]*', method) + return cls._infer_command(words, op_group_name, []) + + @classmethod + def _infer_command(cls, operation, op_group_name, suffix): + operation = [w for w in operation if w != 'All'] + if len(operation) == 1: + # simple operation, just an id with a single value + return [ + cls.create_command_variant(operation[0], [op_group_name], suffix) + ] + + if len(operation) == 2: + # should try to infer [SUBJECT] and [ACTION] from operation + if VERB_MAPPING.get(operation[0]): + # [ACTION][SUBJECT] + return [ + cls.create_command_variant(operation[0], [op_group_name, operation[1]], suffix) + ] + if VERB_MAPPING.get(operation[1]): + # [SUBJECT][ACTION] + return [ + cls.create_command_variant(operation[1], [op_group_name, operation[0]], suffix) + ] + logger.warning(f"Operation ${operation[0]}/${operation[1]} is inferred without finding action.") + return [ + cls.create_command_variant(operation[0], [op_group_name, operation[1]], suffix) + ] + + # three or more words. + # first, see if it's an 'or' + if 'Or' in operation: + idx = operation.index('Or') + return cls._infer_command( + operation[:idx] + operation[idx+2:], + op_group_name, + suffix + ) + cls._infer_command( + operation[idx+1:], + op_group_name, + suffix + ) + + for w in ['With', 'At', 'By', 'For', 'In', 'Of']: + if w in operation: + idx = operation.index(w) + if idx > 0: + # so this is something like DoActionWithStyle + return cls._infer_command( + operation[:idx], + op_group_name, + operation[idx:], + ) + + # if not, then seek out a verb from there. + for i in range(len(operation)): + if VERB_MAPPING.get(operation[i]): + # if the action is first + if i == 0: + # everything else is the subject + return [ + cls.create_command_variant(operation[0], [op_group_name] + operation[1:], suffix) + ] + if i == len(operation) - 1: + # if it's last, the subject would be the first thing + return [ + cls.create_command_variant(operation[i], [op_group_name] + operation[:i], suffix) + ] + # otherwise + # things before are part of the subject + # things after the verb should be part of the suffix + return [ + cls.create_command_variant(operation[i], [op_group_name] + operation[:i], suffix + operation[i+1:]) + ] + + # so couldn't tell what the action was. + # fallback to the original behavior with a warning. + logger.warning(f"Operation ${operation[0]}/${operation[1]} is inferred without finding action.") + return [ + cls.create_command_variant(operation[0], [op_group_name] + operation[1:], suffix) + ] + + @classmethod + def create_command_variant(cls, action, subject, variant): + verb = cls.get_powershell_verb(action) + if verb == 'Invoke': + # if the 'operation' name was "post" -- it's kindof redundant. + # so, only include the operation name in the group name if it's anything else + if action.lower() != 'post': + subject = [action] + subject + subject = [cls.singular_noun(s) or s for s in subject] + # remove duplicate values + values = set() + simplified_subject = [] + for s in subject: + if s in values: + continue + values.add(s) + simplified_subject.append(s) + return { + "subject": ''.join(simplified_subject), + "verb": verb, + "variant": ''.join(variant), + "action": action, + } + + @staticmethod + def get_powershell_verb(action): + action = action[0].upper() + action[1:].lower() + return VERB_MAPPING.get(action, "Invoke") + + @classmethod + def singular_noun(cls, noun): + words = re.findall(r'[A-Z][a-z]*', noun) + # singular the last word in operation group name + w = cls._pluralizer.singular(words[-1].lower()) + words[-1] = w[0].upper() + w[1:] + noun = ''.join(words) + return noun + + +VERB_MAPPING = { + "Access": "Get", + "Acquire": "Get", + "Activate": "Initialize", + "Add": "Add", + "Allocate": "New", + "Analyze": "Test", + "Append": "Add", + "Apply": "Add", + "Approve": "Approve", + "Assert": "Assert", + "Assign": "Set", + "Associate": "Join", + "Attach": "Add", + "Authorize": "Grant", + "Backup": "Backup", + "Block": "Block", + "Build": "Build", + "Bypass": "Skip", + "Cancel": "Stop", + "Capture": "Export", + "Cat": "Get", + "Change": "Rename", + "Check": "Test", + "Checkpoint": "Checkpoint", + "Clear": "Clear", + "Clone": "Copy", + "Close": "Close", + "Combine": "Join", + "Compare": "Compare", + "Compile": "Build", + "Complete": "Complete", + "Compress": "Compress", + "Concatenate": "Add", + "Configure": "Set", + "Confirm": "Confirm", + "Connect": "Connect", + "Convert": "Convert", + "ConvertFrom": "ConvertFrom", + "ConvertTo": "ConvertTo", + "Copy": "Copy", + "Create": "New", + "Cut": "Remove", + "Debug": "Debug", + "Delete": "Remove", + "Deny": "Deny", + "Deploy": "Deploy", + "Dir": "Get", + "Disable": "Disable", + "Discard": "Remove", + "Disconnect": "Disconnect", + "Discover": "Find", + "Dismount": "Dismount", + "Display": "Show", + "Dispose": "Remove", + "Dump": "Get", + "Duplicate": "Copy", + "Edit": "Edit", + "Enable": "Enable", + "End": "Stop", + "Enter": "Enter", + "Erase": "Clear", + "Evaluate": "Test", + "Examine": "Get", + "Execute": "Invoke", + "Exit": "Exit", + "Expand": "Expand", + "Export": "Export", + "Failover": "Set", + "Find": "Find", + "Finish": "Complete", + "Flush": "Clear", + "ForceReboot": "Restart", + "Format": "Format", + "Generalize": "Reset", + "Generate": "New", + "Get": "Get", + "Grant": "Grant", + "Group": "Group", + "Hide": "Hide", + "Import": "Import", + "Initialize": "Initialize", + "Insert": "Add", + "Install": "Install", + "Into": "Enter", + "Invoke": "Invoke", + "Is": "Test", + "Join": "Join", + "Jump": "Skip", + "Limit": "Limit", + "List": "Get", + "Load": "Import", + "Locate": "Find", + "Lock": "Lock", + "Make": "New", + "Measure": "Measure", + "Merge": "Merge", + "Migrate": "Move", + "Mount": "Mount", + "Move": "Move", + "Name": "Move", + "New": "New", + "Notify": "Send", + "Nullify": "Clear", + "Obtain": "Get", + "Open": "Open", + "Optimize": "Optimize", + "Out": "Out", + "Patch": "Update", + "Pause": "Suspend", + "Perform": "Invoke", + "Ping": "Ping", + "Pop": "Pop", + "Post": "Invoke", + "Power": "Start", + "PowerOff": "Stop", + "PowerOn": "Start", + "Produce": "Show", + "Protect": "Protect", + "Provision": "New", + "Publish": "Publish", + "Purge": "Clear", + "Push": "Push", + "Put": "Set", + "Read": "Read", + "Reassociate": "Move", + "Reboot": "Restart", + "Receive": "Receive", + "Recover": "Restore", + "Redo": "Redo", + "Refresh": "Update", + "Regenerate": "New", + "Register": "Register", + "Reimage": "Update", + "Release": "Publish", + "Remove": "Remove", + "Rename": "Rename", + "Repair": "Repair", + "Replace": "Update", + "Replicate": "Copy", + "Reprocess": "Update", + "Request": "Request", + "Reset": "Reset", + "Resize": "Resize", + "Resolve": "Resolve", + "Restart": "Restart", + "Restore": "Restore", + "Restrict": "Lock", + "Resubmit": "Submit", + "Resume": "Resume", + "Retarget": "Update", + "Retrieve": "Get", + "Revoke": "Revoke", + "Run": "Start", + "Save": "Save", + "Search": "Search", + "Secure": "Lock", + "Select": "Select", + "Send": "Send", + "Separate": "Split", + "Set": "Set", + "Show": "Show", + "Shutdown": "Stop", + "Skip": "Skip", + "Split": "Split", + "Start": "Start", + "Step": "Step", + "Stop": "Stop", + "Submit": "Submit", + "Suggest": "Get", + "Suspend": "Suspend", + "Swap": "Switch", + "Switch": "Switch", + "Sync": "Sync", + "Synch": "Sync", + "Synchronize": "Sync", + "Test": "Test", + "Trace": "Trace", + "Transfer": "Move", + "Trigger": "Start", + "Type": "Get", + "Unblock": "Unblock", + "Undelete": "Restore", + "Undo": "Undo", + "Uninstall": "Uninstall", + "Unite": "Join", + "Unlock": "Unlock", + "Unmark": "Clear", + "Unprotect": "Unprotect", + "Unpublish": "Unpublish", + "Unregister": "Unregister", + "Unrestrict": "Unlock", + "Unsecure": "Unlock", + "Unset": "Clear", + "Update": "Update", + "Upgrade": "Update", + "Use": "Use", + "Validate": "Test", + "Verify": "Test", + "Wait": "Wait", + "Watch": "Watch", + "Wipe": "Clear", + "Write": "Write", +} diff --git a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 index 8e9f8f1a..67440422 100644 --- a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 +++ b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 @@ -1,11 +1,45 @@ +### AutoRest Configuration +> see https://aka.ms/autorest +```yaml +commit: {{ cfg.commit }} +require: + - $(this-folder)/../../readme.azure.noprofile.md + - $(repo)/{{ cfg.readme_file }} + +try-require: + - $(repo)/{{ cfg.readme_file }} + +module-version: {{ cfg.version }} +title: {{ cfg.module_name }} +subject-prefix: $(service-name) + +inlining-threshold: 100 +resourcegroup-append: true +nested-object-to-string: true +identity-correction-for-post: true directive: + # Model complex objects + # Remove the set-* cmdlet - where: verb: Set remove: true + # Remove APIs + {%- if cfg.removed_subjects|length %} + - where: + subject: {{ cfg.removed_subjects | join("|") }} + remove: true} + {%- endif %} + {%- for subject, verb in cfg.removed_verbs %} + - where: + subject: {{ subject }} + verb: {{ verb }} + remove: true + {%- endfor %} + # Remove variants - where: variant: ^(Create|Update)(?!.*?(Expanded|JsonFilePath|JsonString)) @@ -14,11 +48,6 @@ directive: variant: ^CreateViaIdentity.*$ remove: true - # TODO: Remove variants with post verb - - # Remove APIs - - - # TODO: Rename parameter + # Rename parameter - # TODO: Model complex objects +``` \ No newline at end of file diff --git a/src/aaz_dev/command/model/configuration/_arg_builder.py b/src/aaz_dev/command/model/configuration/_arg_builder.py index 6b376443..ab82aff1 100644 --- a/src/aaz_dev/command/model/configuration/_arg_builder.py +++ b/src/aaz_dev/command/model/configuration/_arg_builder.py @@ -1,6 +1,6 @@ import re -import inflect +from pluralizer import Pluralizer from utils.case import to_camel_case from ._arg import CMDArg, CMDArgBase, CMDArgumentHelp, CMDArgEnum, CMDArgDefault, CMDBooleanArgBase, \ @@ -14,7 +14,7 @@ class CMDArgBuilder: - _inflect_engine = inflect.engine() + _pluralizer = Pluralizer() @classmethod def new_builder(cls, schema, parent=None, var_prefix=None, ref_args=None, ref_arg=None, is_update_action=False): @@ -364,7 +364,7 @@ def get_options(self): if name == "[Index]" or name == "{Key}": assert self._arg_var.endswith(name) prefix = self._arg_var[:-len(name)].split('.')[-1] - prefix = self._inflect_engine.singular_noun(prefix) + prefix = self._pluralizer.singular(prefix) if name == "[Index]": name = f'{prefix}-index' elif name == "{Key}": @@ -372,7 +372,7 @@ def get_options(self): elif name.startswith('[].') or name.startswith('{}.'): assert self._arg_var.endswith(name) prefix = self._arg_var[:-len(name)].split('.')[-1] - prefix = self._inflect_engine.singular_noun(prefix) + prefix = self._pluralizer.singular(prefix) name = prefix + name[2:] name = name.replace('.', '-') opt_name = self._build_option_name(name) # some schema name may contain $ @@ -392,7 +392,7 @@ def get_singular_options(self): # Disable singular options by default # if isinstance(self.schema, CMDArraySchema): # opt_name = self._build_option_name(self.schema.name.replace('$', '')) # some schema name may contain $ - # singular_opt_name = self._inflect_engine.singular_noun(opt_name) or opt_name + # singular_opt_name = self._pluralizer.singular(opt_name) or opt_name # if singular_opt_name != opt_name: # return [singular_opt_name, ] return None diff --git a/src/aaz_dev/command/model/configuration/_xml.py b/src/aaz_dev/command/model/configuration/_xml.py index 69ad4991..c9655241 100644 --- a/src/aaz_dev/command/model/configuration/_xml.py +++ b/src/aaz_dev/command/model/configuration/_xml.py @@ -1,4 +1,4 @@ -import inflect +from pluralizer import Pluralizer import re from lxml.builder import ElementMaker @@ -49,7 +49,7 @@ def build_xml(primitive, parent=None): if parent is None: parent = getattr(ElementMaker(), XML_ROOT)() # normalize element name - if elem_name := _inflect_engine.singular_noun(parent.tag): + if elem_name := _pluralizer.singular(parent.tag): parent.tag = elem_name for field_name, data in primitive.items(): primitive_to_xml(field_name, data, parent) @@ -87,7 +87,7 @@ def build_model(model, primitive): # obtain suitable element name if serialized_name in primitive: curr_name = serialized_name - elif (elem_name := _inflect_engine.singular_noun(serialized_name)) in primitive: + elif (elem_name := _pluralizer.singular(serialized_name)) in primitive: curr_name = elem_name else: continue @@ -137,4 +137,4 @@ def _unwrap(field): return field -_inflect_engine = inflect.engine() +_pluralizer = Pluralizer() diff --git a/src/aaz_dev/ps_profile.json b/src/aaz_dev/ps_profile.json index 3c5ba799..feb009b4 100644 --- a/src/aaz_dev/ps_profile.json +++ b/src/aaz_dev/ps_profile.json @@ -2,48 +2,116 @@ "AutonomousDatabase": { "operations": { "AutonomousDatabases_ListBySubscription": { + "tag": "AutonomousDatabases_ListBySubscription", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/autonomousdatabases", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Get", + "variant": "BySubscription", + "action": "List" + } + ] }, "AutonomousDatabases_ListByResourceGroup": { + "tag": "AutonomousDatabases_ListByResourceGroup", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Get", + "variant": "ByResourceGroup", + "action": "List" + } + ] }, "AutonomousDatabases_CreateOrUpdate": { + "tag": "AutonomousDatabases_CreateOrUpdate", "delete": false, - "method": "put" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}", + "method": "put", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "New", + "variant": "", + "action": "Create" + } + ] }, "AutonomousDatabases_Delete": { + "tag": "AutonomousDatabases_Delete", "delete": false, - "method": "delete" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}", + "method": "delete", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Remove", + "variant": "", + "action": "Delete" + } + ] }, "AutonomousDatabases_Get": { + "tag": "AutonomousDatabases_Get", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] }, "AutonomousDatabases_Update": { + "tag": "AutonomousDatabases_Update", "delete": false, - "method": "patch" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}", + "method": "patch", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] }, "AutonomousDatabases_Failover": { + "tag": "AutonomousDatabases_Failover", "delete": false, - "method": "post" - }, - "AutonomousDatabases_GenerateWallet": { - "delete": true, - "method": "post" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/failover", + "method": "post", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Set", + "variant": "", + "action": "Failover" + } + ] }, "AutonomousDatabases_Restore": { + "tag": "AutonomousDatabases_Restore", "delete": false, - "method": "post" - }, - "AutonomousDatabases_Shrink": { - "delete": true, - "method": "post" - }, - "AutonomousDatabases_Switchover": { - "delete": false, - "method": "post" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/restore", + "method": "post", + "variants": [ + { + "subject": "AutonomousDatabase", + "verb": "Restore", + "variant": "", + "action": "Restore" + } + ] } }, "delete": false @@ -51,32 +119,88 @@ "CloudExadataInfrastructure": { "operations": { "CloudExadataInfrastructures_ListBySubscription": { + "tag": "CloudExadataInfrastructures_ListBySubscription", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/cloudexadatainfrastructures", + "method": "get", + "variants": [ + { + "subject": "CloudExadataInfrastructure", + "verb": "Get", + "variant": "BySubscription", + "action": "List" + } + ] }, "CloudExadataInfrastructures_ListByResourceGroup": { + "tag": "CloudExadataInfrastructures_ListByResourceGroup", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures", + "method": "get", + "variants": [ + { + "subject": "CloudExadataInfrastructure", + "verb": "Get", + "variant": "ByResourceGroup", + "action": "List" + } + ] }, "CloudExadataInfrastructures_CreateOrUpdate": { + "tag": "CloudExadataInfrastructures_CreateOrUpdate", "delete": false, - "method": "put" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}", + "method": "put", + "variants": [ + { + "subject": "CloudExadataInfrastructure", + "verb": "New", + "variant": "", + "action": "Create" + } + ] }, "CloudExadataInfrastructures_Delete": { + "tag": "CloudExadataInfrastructures_Delete", "delete": false, - "method": "delete" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}", + "method": "delete", + "variants": [ + { + "subject": "CloudExadataInfrastructure", + "verb": "Remove", + "variant": "", + "action": "Delete" + } + ] }, "CloudExadataInfrastructures_Get": { + "tag": "CloudExadataInfrastructures_Get", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}", + "method": "get", + "variants": [ + { + "subject": "CloudExadataInfrastructure", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] }, "CloudExadataInfrastructures_Update": { + "tag": "CloudExadataInfrastructures_Update", "delete": false, - "method": "patch" - }, - "CloudExadataInfrastructures_AddStorageCapacity": { - "delete": true, - "method": "post" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}", + "method": "patch", + "variants": [ + { + "subject": "CloudExadataInfrastructure", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] } }, "delete": false @@ -84,40 +208,88 @@ "CloudVmCluster": { "operations": { "CloudVmClusters_ListBySubscription": { + "tag": "CloudVmClusters_ListBySubscription", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/cloudvmclusters", + "method": "get", + "variants": [ + { + "subject": "CloudVmCluster", + "verb": "Get", + "variant": "BySubscription", + "action": "List" + } + ] }, "CloudVmClusters_ListByResourceGroup": { + "tag": "CloudVmClusters_ListByResourceGroup", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters", + "method": "get", + "variants": [ + { + "subject": "CloudVmCluster", + "verb": "Get", + "variant": "ByResourceGroup", + "action": "List" + } + ] }, "CloudVmClusters_CreateOrUpdate": { + "tag": "CloudVmClusters_CreateOrUpdate", "delete": false, - "method": "put" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}", + "method": "put", + "variants": [ + { + "subject": "CloudVmCluster", + "verb": "New", + "variant": "", + "action": "Create" + } + ] }, "CloudVmClusters_Delete": { + "tag": "CloudVmClusters_Delete", "delete": false, - "method": "delete" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}", + "method": "delete", + "variants": [ + { + "subject": "CloudVmCluster", + "verb": "Remove", + "variant": "", + "action": "Delete" + } + ] }, "CloudVmClusters_Get": { + "tag": "CloudVmClusters_Get", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}", + "method": "get", + "variants": [ + { + "subject": "CloudVmCluster", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] }, "CloudVmClusters_Update": { + "tag": "CloudVmClusters_Update", "delete": false, - "method": "patch" - }, - "CloudVmClusters_AddVms": { - "delete": false, - "method": "post" - }, - "CloudVmClusters_ListPrivateIpAddresses": { - "delete": true, - "method": "post" - }, - "CloudVmClusters_RemoveVms": { - "delete": false, - "method": "post" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}", + "method": "patch", + "variants": [ + { + "subject": "CloudVmCluster", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] } }, "delete": false @@ -125,12 +297,32 @@ "AutonomousDatabaseCharacterSet": { "operations": { "AutonomousDatabaseCharacterSets_ListByLocation": { + "tag": "AutonomousDatabaseCharacterSets_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/autonomousdatabasecharactersets", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseCharacterSet", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "AutonomousDatabaseCharacterSets_Get": { + "tag": "AutonomousDatabaseCharacterSets_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/autonomousdatabasecharactersets/{}", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseCharacterSet", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -138,12 +330,32 @@ "AutonomousDatabaseNationalCharacterSet": { "operations": { "AutonomousDatabaseNationalCharacterSets_ListByLocation": { + "tag": "AutonomousDatabaseNationalCharacterSets_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/autonomousdatabasenationalcharactersets", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseNationalCharacterSet", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "AutonomousDatabaseNationalCharacterSets_Get": { + "tag": "AutonomousDatabaseNationalCharacterSets_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/autonomousdatabasenationalcharactersets/{}", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseNationalCharacterSet", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -151,12 +363,32 @@ "AutonomousDatabaseVersion": { "operations": { "AutonomousDatabaseVersions_ListByLocation": { + "tag": "AutonomousDatabaseVersions_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/autonomousdbversions", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseVersion", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "AutonomousDatabaseVersions_Get": { + "tag": "AutonomousDatabaseVersions_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/autonomousdbversions/{}", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseVersion", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -164,12 +396,32 @@ "DbSystemShape": { "operations": { "DbSystemShapes_ListByLocation": { + "tag": "DbSystemShapes_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/dbsystemshapes", + "method": "get", + "variants": [ + { + "subject": "DbSystemShape", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "DbSystemShapes_Get": { + "tag": "DbSystemShapes_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/dbsystemshapes/{}", + "method": "get", + "variants": [ + { + "subject": "DbSystemShape", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -177,12 +429,32 @@ "DnsPrivateView": { "operations": { "DnsPrivateViews_ListByLocation": { + "tag": "DnsPrivateViews_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/dnsprivateviews", + "method": "get", + "variants": [ + { + "subject": "DnsPrivateView", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "DnsPrivateViews_Get": { + "tag": "DnsPrivateViews_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/dnsprivateviews/{}", + "method": "get", + "variants": [ + { + "subject": "DnsPrivateView", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -190,12 +462,32 @@ "DnsPrivateZone": { "operations": { "DnsPrivateZones_ListByLocation": { + "tag": "DnsPrivateZones_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/dnsprivatezones", + "method": "get", + "variants": [ + { + "subject": "DnsPrivateZone", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "DnsPrivateZones_Get": { + "tag": "DnsPrivateZones_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/dnsprivatezones/{}", + "method": "get", + "variants": [ + { + "subject": "DnsPrivateZone", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -203,12 +495,32 @@ "GiVersion": { "operations": { "GiVersions_ListByLocation": { + "tag": "GiVersions_ListByLocation", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/giversions", + "method": "get", + "variants": [ + { + "subject": "GiVersion", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "GiVersions_Get": { + "tag": "GiVersions_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/giversions/{}", + "method": "get", + "variants": [ + { + "subject": "GiVersion", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": false @@ -216,12 +528,32 @@ "SystemVersion": { "operations": { "SystemVersions_ListByLocation": { + "tag": "SystemVersions_ListByLocation", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/systemversions", + "method": "get", + "variants": [ + { + "subject": "SystemVersion", + "verb": "Get", + "variant": "ByLocation", + "action": "List" + } + ] }, "SystemVersions_Get": { + "tag": "SystemVersions_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/locations/{}/systemversions/{}", + "method": "get", + "variants": [ + { + "subject": "SystemVersion", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] } }, "delete": true @@ -229,40 +561,156 @@ "OracleSubscription": { "operations": { "OracleSubscriptions_ListBySubscription": { + "tag": "OracleSubscriptions_ListBySubscription", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions", + "method": "get", + "variants": [ + { + "subject": "OracleSubscription", + "verb": "Get", + "variant": "BySubscription", + "action": "List" + } + ] }, "OracleSubscriptions_Get": { + "tag": "OracleSubscriptions_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default", + "method": "get", + "variants": [ + { + "subject": "OracleSubscription", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] }, "OracleSubscriptions_CreateOrUpdate": { + "tag": "OracleSubscriptions_CreateOrUpdate", "delete": true, - "method": "put" + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default", + "method": "put", + "variants": [ + { + "subject": "OracleSubscription", + "verb": "New", + "variant": "", + "action": "Create" + }, + { + "subject": "OracleSubscription", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] }, "OracleSubscriptions_Update": { + "tag": "OracleSubscriptions_Update", "delete": true, - "method": "patch" + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default", + "method": "patch", + "variants": [ + { + "subject": "OracleSubscription", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] }, "OracleSubscriptions_Delete": { + "tag": "OracleSubscriptions_Delete", "delete": true, - "method": "delete" - }, + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default", + "method": "delete", + "variants": [ + { + "subject": "OracleSubscription", + "verb": "Remove", + "variant": "", + "action": "Delete" + } + ] + } + }, + "delete": true + }, + "OracleSubscriptionAzureSubscription": { + "operations": { "OracleSubscriptions_AddAzureSubscriptions": { + "tag": "OracleSubscriptions_AddAzureSubscriptions", "delete": true, - "method": "post" - }, + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default/addazuresubscriptions", + "method": "post", + "variants": [ + { + "subject": "OracleSubscriptionAzureSubscription", + "verb": "Add", + "variant": "", + "action": "Add" + } + ] + } + }, + "delete": true + }, + "OracleSubscriptionActivationLink": { + "operations": { "OracleSubscriptions_ListActivationLinks": { + "tag": "OracleSubscriptions_ListActivationLinks", "delete": true, - "method": "post" - }, + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default/listactivationlinks", + "method": "post", + "variants": [ + { + "subject": "OracleSubscriptionActivationLink", + "verb": "Get", + "variant": "", + "action": "List" + } + ] + } + }, + "delete": true + }, + "OracleSubscriptionCloudAccountDetail": { + "operations": { "OracleSubscriptions_ListCloudAccountDetails": { + "tag": "OracleSubscriptions_ListCloudAccountDetails", "delete": true, - "method": "post" - }, + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default/listcloudaccountdetails", + "method": "post", + "variants": [ + { + "subject": "OracleSubscriptionCloudAccountDetail", + "verb": "Get", + "variant": "", + "action": "List" + } + ] + } + }, + "delete": true + }, + "OracleSubscriptionSaaSubscriptionDetail": { + "operations": { "OracleSubscriptions_ListSaasSubscriptionDetails": { + "tag": "OracleSubscriptions_ListSaasSubscriptionDetails", "delete": true, - "method": "post" + "resource_id": "/subscriptions/{}/providers/oracle.database/oraclesubscriptions/default/listsaassubscriptiondetails", + "method": "post", + "variants": [ + { + "subject": "OracleSubscriptionSaaSubscriptionDetail", + "verb": "Get", + "variant": "", + "action": "List" + } + ] } }, "delete": true @@ -270,37 +718,216 @@ "AutonomousDatabaseBackup": { "operations": { "AutonomousDatabaseBackups_ListByAutonomousDatabase": { + "tag": "AutonomousDatabaseBackups_ListByAutonomousDatabase", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/autonomousdatabasebackups", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseBackup", + "verb": "Get", + "variant": "ByAutonomousDatabase", + "action": "List" + } + ] }, "AutonomousDatabaseBackups_CreateOrUpdate": { + "tag": "AutonomousDatabaseBackups_CreateOrUpdate", "delete": false, - "method": "put" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/autonomousdatabasebackups/{}", + "method": "put", + "variants": [ + { + "subject": "AutonomousDatabaseBackup", + "verb": "New", + "variant": "", + "action": "Create" + } + ] }, "AutonomousDatabaseBackups_Delete": { + "tag": "AutonomousDatabaseBackups_Delete", "delete": false, - "method": "delete" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/autonomousdatabasebackups/{}", + "method": "delete", + "variants": [ + { + "subject": "AutonomousDatabaseBackup", + "verb": "Remove", + "variant": "", + "action": "Delete" + } + ] }, "AutonomousDatabaseBackups_Get": { + "tag": "AutonomousDatabaseBackups_Get", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/autonomousdatabasebackups/{}", + "method": "get", + "variants": [ + { + "subject": "AutonomousDatabaseBackup", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] }, "AutonomousDatabaseBackups_Update": { + "tag": "AutonomousDatabaseBackups_Update", + "delete": false, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/autonomousdatabasebackups/{}", + "method": "patch", + "variants": [ + { + "subject": "AutonomousDatabaseBackup", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] + } + }, + "delete": false + }, + "AutonomousDatabaseWallet": { + "operations": { + "AutonomousDatabases_GenerateWallet": { + "tag": "AutonomousDatabases_GenerateWallet", + "delete": true, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/generatewallet", + "method": "post", + "variants": [ + { + "subject": "AutonomousDatabaseWallet", + "verb": "New", + "variant": "", + "action": "Generate" + } + ] + } + }, + "delete": true + }, + "ShrinkAutonomousDatabase": { + "operations": { + "AutonomousDatabases_Shrink": { + "tag": "AutonomousDatabases_Shrink", + "delete": true, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/shrink", + "method": "post", + "variants": [ + { + "subject": "ShrinkAutonomousDatabase", + "verb": "Invoke", + "variant": "", + "action": "Shrink" + } + ] + } + }, + "delete": true + }, + "SwitchoverAutonomousDatabase": { + "operations": { + "AutonomousDatabases_Switchover": { + "tag": "AutonomousDatabases_Switchover", "delete": false, - "method": "patch" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/autonomousdatabases/{}/switchover", + "method": "post", + "variants": [ + { + "subject": "SwitchoverAutonomousDatabase", + "verb": "Invoke", + "variant": "", + "action": "Switchover" + } + ] } }, "delete": false }, + "CloudExadataInfrastructureStorageCapacity": { + "operations": { + "CloudExadataInfrastructures_AddStorageCapacity": { + "tag": "CloudExadataInfrastructures_AddStorageCapacity", + "delete": true, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}/addstoragecapacity", + "method": "post", + "variants": [ + { + "subject": "CloudExadataInfrastructureStorageCapacity", + "verb": "Add", + "variant": "", + "action": "Add" + } + ] + } + }, + "delete": true + }, "DbServer": { "operations": { "DbServers_ListByCloudExadataInfrastructure": { + "tag": "DbServers_ListByCloudExadataInfrastructure", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}/dbservers", + "method": "get", + "variants": [ + { + "subject": "DbServer", + "verb": "Get", + "variant": "ByCloudExadataInfrastructure", + "action": "List" + } + ] }, "DbServers_Get": { + "tag": "DbServers_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudexadatainfrastructures/{}/dbservers/{}", + "method": "get", + "variants": [ + { + "subject": "DbServer", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] + } + }, + "delete": false + }, + "CloudVmClusterVm": { + "operations": { + "CloudVmClusters_AddVms": { + "tag": "CloudVmClusters_AddVms", + "delete": false, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/addvms", + "method": "post", + "variants": [ + { + "subject": "CloudVmClusterVm", + "verb": "Add", + "variant": "", + "action": "Add" + } + ] + }, + "CloudVmClusters_RemoveVms": { + "tag": "CloudVmClusters_RemoveVms", + "delete": false, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/removevms", + "method": "post", + "variants": [ + { + "subject": "CloudVmClusterVm", + "verb": "Remove", + "variant": "", + "action": "Remove" + } + ] } }, "delete": false @@ -308,37 +935,137 @@ "DbNode": { "operations": { "DbNodes_ListByCloudVmCluster": { + "tag": "DbNodes_ListByCloudVmCluster", "delete": false, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/dbnodes", + "method": "get", + "variants": [ + { + "subject": "DbNode", + "verb": "Get", + "variant": "ByCloudVmCluster", + "action": "List" + } + ] }, "DbNodes_Get": { + "tag": "DbNodes_Get", "delete": true, - "method": "get" - }, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/dbnodes/{}", + "method": "get", + "variants": [ + { + "subject": "DbNode", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] + } + }, + "delete": false + }, + "ActionDbNode": { + "operations": { "DbNodes_Action": { + "tag": "DbNodes_Action", "delete": false, - "method": "post" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/dbnodes/{}/action", + "method": "post", + "variants": [ + { + "subject": "ActionDbNode", + "verb": "Invoke", + "variant": "", + "action": "Action" + } + ] } }, "delete": false }, + "CloudVmClusterPrivateIpAddress": { + "operations": { + "CloudVmClusters_ListPrivateIpAddresses": { + "tag": "CloudVmClusters_ListPrivateIpAddresses", + "delete": true, + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/listprivateipaddresses", + "method": "post", + "variants": [ + { + "subject": "CloudVmClusterPrivateIpAddress", + "verb": "Get", + "variant": "", + "action": "List" + } + ] + } + }, + "delete": true + }, "VirtualNetworkAddress": { "operations": { "VirtualNetworkAddresses_ListByCloudVmCluster": { + "tag": "VirtualNetworkAddresses_ListByCloudVmCluster", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/virtualnetworkaddresses", + "method": "get", + "variants": [ + { + "subject": "VirtualNetworkAddress", + "verb": "Get", + "variant": "ByCloudVmCluster", + "action": "List" + } + ] }, "VirtualNetworkAddresses_Get": { + "tag": "VirtualNetworkAddresses_Get", "delete": true, - "method": "get" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/virtualnetworkaddresses/{}", + "method": "get", + "variants": [ + { + "subject": "VirtualNetworkAddress", + "verb": "Get", + "variant": "", + "action": "Get" + } + ] }, "VirtualNetworkAddresses_CreateOrUpdate": { + "tag": "VirtualNetworkAddresses_CreateOrUpdate", "delete": true, - "method": "put" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/virtualnetworkaddresses/{}", + "method": "put", + "variants": [ + { + "subject": "VirtualNetworkAddress", + "verb": "New", + "variant": "", + "action": "Create" + }, + { + "subject": "VirtualNetworkAddress", + "verb": "Update", + "variant": "", + "action": "Update" + } + ] }, "VirtualNetworkAddresses_Delete": { + "tag": "VirtualNetworkAddresses_Delete", "delete": true, - "method": "delete" + "resource_id": "/subscriptions/{}/resourcegroups/{}/providers/oracle.database/cloudvmclusters/{}/virtualnetworkaddresses/{}", + "method": "delete", + "variants": [ + { + "subject": "VirtualNetworkAddress", + "verb": "Remove", + "variant": "", + "action": "Delete" + } + ] } }, "delete": true diff --git a/src/aaz_dev/swagger/controller/command_generator.py b/src/aaz_dev/swagger/controller/command_generator.py index 3ca5a792..ec410059 100644 --- a/src/aaz_dev/swagger/controller/command_generator.py +++ b/src/aaz_dev/swagger/controller/command_generator.py @@ -1,7 +1,7 @@ import logging import re -import inflect +from pluralizer import Pluralizer from abc import abstractmethod, ABC from command.model.configuration import CMDCommandGroup, CMDCommand, CMDHttpOperation, CMDHttpRequest, \ @@ -25,7 +25,7 @@ class _CommandGenerator(ABC): - _inflect_engine = inflect.engine() + _pluralizer = Pluralizer() @staticmethod def generate_command_version(resource): @@ -161,7 +161,7 @@ def generate_command_group_name_by_resource(cls, resource_path, rp_name): part = re.sub(r"\{[^{}]*}", '', part) part = re.sub(r"[^a-zA-Z0-9\-._]", '', part) name = camel_case_to_snake_case(part, '-') - singular_name = cls._inflect_engine.singular_noun(name) or name + singular_name = cls._pluralizer.singular(name) or name names.append(singular_name) return " ".join([name for name in names if name]) diff --git a/src/aaz_dev/swagger/model/specs/_resource.py b/src/aaz_dev/swagger/model/specs/_resource.py index 674c92bb..3a8703d0 100644 --- a/src/aaz_dev/swagger/model/specs/_resource.py +++ b/src/aaz_dev/swagger/model/specs/_resource.py @@ -4,7 +4,7 @@ import os import re -import inflect +from pluralizer import Pluralizer from fuzzywuzzy import fuzz from command.model.configuration import CMDResource @@ -17,7 +17,7 @@ class Resource: _CAMEL_CASE_PATTERN = re.compile(r"^([a-zA-Z][a-z0-9]+)(([A-Z][a-z0-9]*)+)$") - _inflect_engine = inflect.engine() + _pluralizer = Pluralizer() def __init__(self, resource_id, path, version, file_path, resource_provider, body): self.path = path @@ -98,22 +98,22 @@ def _parse_operation_group_name(self, op_id, method): words = [] for part in self.id.split('?')[0].split('/'): if part == '{}' and len(words): - singular = self._inflect_engine.singular_noun(words[-1]) + singular = self._pluralizer.singular(words[-1]) if singular: words[-1] = singular else: words.append(part.replace('_', "")) - op_group_singular = self._inflect_engine.singular_noun(op_group_name) or op_group_name + op_group_singular = self._pluralizer.singular(op_group_name) or op_group_name words.reverse() # search from tail for word in words: - word_singular = self._inflect_engine.singular_noun(word) or word + word_singular = self._pluralizer.singular(word) or word if len(word_singular) > 1 and op_group_singular.lower().endswith(word_singular.lower()): if word == word_singular: # use singular op_group_name = op_group_singular elif word != word_singular: # use plural - op_group_plural = self._inflect_engine.plural_noun(op_group_singular) + op_group_plural = self._pluralizer.singular(op_group_singular) if op_group_plural is not False: op_group_name = op_group_plural break From be44813e421c66a9031db5f3a9a07262339048f9 Mon Sep 17 00:00:00 2001 From: kai ru Date: Thu, 19 Sep 2024 16:16:08 +0800 Subject: [PATCH 4/5] Implement the autorest generation --- src/aaz_dev/cli/api/_cmds.py | 73 ++++++++++++++++++- .../cli/controller/ps_config_generator.py | 1 + .../powershell/configuration.yaml.j2 | 2 +- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/aaz_dev/cli/api/_cmds.py b/src/aaz_dev/cli/api/_cmds.py index 4eabb9f2..91622b2d 100644 --- a/src/aaz_dev/cli/api/_cmds.py +++ b/src/aaz_dev/cli/api/_cmds.py @@ -2,6 +2,8 @@ import logging from flask import Blueprint import sys +import subprocess +import os from utils.config import Config @@ -261,6 +263,12 @@ def _build_profile(profile_name, commands_map): callback=Config.validate_and_setup_cli_extension_path, help="The local path of azure-cli-extension repo. Only required when generate from azure-cli extension." ) +@click.option( + "--powershell-path", '-p', + type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True), + required=True, + help="The local path of azure-powershell repo." +) @click.option( "--extension-or-module-name", '--name', required=True, @@ -275,10 +283,18 @@ def _build_profile(profile_name, commands_map): expose_value=False, help="The local path of azure-rest-api-specs repo. Official repo is https://github.com/Azure/azure-rest-api-specs" ) -def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_path=None): +def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_path=None, powershell_path=None): from cli.controller.ps_config_generator import PSAutoRestConfigurationGenerator from cli.controller.az_module_manager import AzMainManager, AzExtensionManager from cli.templates import get_templates + + # Module path in azure-powershell repo + + powershell_path = os.path.join(powershell_path, "src") + if not os.path.exists(powershell_path): + logger.error(f"Path `{powershell_path}` not exist") + sys.exit(1) + if cli_path is not None: assert Config.CLI_PATH is not None manager = AzMainManager() @@ -291,9 +307,62 @@ def generate_powershell(extension_or_module_name, cli_path=None, cli_extension_p logger.error(f"Cannot find module or extension `{extension_or_module_name}`") sys.exit(1) + # generate README.md for powershell from CLI, ex, for Oracle, README.md should be generated in src/Oracle/Oracle.Autorest/README.md in azure-powershell repo ps_generator = PSAutoRestConfigurationGenerator(manager, extension_or_module_name) ps_cfg = ps_generator.generate_config() + autorest_module_path = os.path.join(powershell_path, ps_cfg.module_name, f"{ps_cfg.module_name}.Autorest") + if not os.path.exists(autorest_module_path): + os.makedirs(autorest_module_path) + readme_file = os.path.join(autorest_module_path, "README.md") + if os.path.exists(readme_file): + # read until to the "### AutoRest Configuration" + with open(readme_file, "r") as f: + lines = f.readlines() + for i, line in enumerate(lines): + if line.startswith("### AutoRest Configuration"): + lines = lines[:i] + break + else: + lines = [] + tmpl = get_templates()['powershell']['configuration'] data = tmpl.render(cfg=ps_cfg) - + lines.append(data) + with open(readme_file, "w") as f: + f.writelines(lines) + + print(f"Generated {readme_file}") + # Generate and build PowerShell module from the README.md file generated above + print("Start to generate the PowerShell module from the README.md file in " + autorest_module_path) + + # Execute autorest to generate the PowerShell module + original_cwd = os.getcwd() + os.chdir(autorest_module_path) + exit_code = os.system("pwsh -Command autorest") + + # Print the output of the generation + if (exit_code != 0): + print("Failed to generate the module") + os.chdir(original_cwd) + sys.exit(1) + else: + print("Code generation succeeded.") + # print(result.stdout) + + os.chdir(original_cwd) + # Execute autorest to generate the PowerShell module + print("Start to build the generated PowerShell module") + result = subprocess.run( + ["pwsh", "-File", 'build-module.ps1'], + capture_output=True, + text=True, + cwd=autorest_module_path + ) + + if (result.returncode != 0): + print("Failed to build the module, please see following output for details:") + print(result.stderr) + sys.exit(1) + else: + print("Module build succeeds, and you may run the generated module by executing the following command: `./run-module.ps1` in " + autorest_module_path) diff --git a/src/aaz_dev/cli/controller/ps_config_generator.py b/src/aaz_dev/cli/controller/ps_config_generator.py index 5def6f79..adb0bce6 100644 --- a/src/aaz_dev/cli/controller/ps_config_generator.py +++ b/src/aaz_dev/cli/controller/ps_config_generator.py @@ -111,6 +111,7 @@ def generate_config(self): ps_cfg.readme_file = '/'.join(readme_parts[readme_parts.index("specification"):]) ps_cfg.version = "0.1.0" ps_cfg.module_name = mod_names.split("/")[0] + ps_cfg.module_name = ps_cfg.module_name[0].upper() + ps_cfg.module_name[1:] ps_profile = {} diff --git a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 index 67440422..dd1cbf90 100644 --- a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 +++ b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 @@ -50,4 +50,4 @@ directive: # Rename parameter -``` \ No newline at end of file +``` From b5deaf98f38867ae3063601f2710df3d7ea941da Mon Sep 17 00:00:00 2001 From: kai ru Date: Mon, 23 Sep 2024 11:19:32 +0800 Subject: [PATCH 5/5] Fix the linker template --- src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 index dd1cbf90..ef8084e0 100644 --- a/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 +++ b/src/aaz_dev/cli/templates/powershell/configuration.yaml.j2 @@ -31,7 +31,7 @@ directive: {%- if cfg.removed_subjects|length %} - where: subject: {{ cfg.removed_subjects | join("|") }} - remove: true} + remove: true {%- endif %} {%- for subject, verb in cfg.removed_verbs %} - where: