Skip to content

Commit 8adf4b4

Browse files
authored
Merge pull request #513 from necusjz/add-liftr-interface
feat: add cli generate interface
2 parents 9402b49 + 86edc40 commit 8adf4b4

26 files changed

+240
-39
lines changed

src/aaz_dev/cli/api/_cmds.py

Lines changed: 194 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import click
22
import logging
33
from flask import Blueprint
4+
import os
45
import sys
56

7+
from cli.controller.az_module_manager import AzExtensionManager, AzMainManager
8+
from command.controller.specs_manager import AAZSpecsManager
9+
from command.controller.workspace_manager import WorkspaceManager
10+
from command.model.configuration import CMDHelp
11+
from swagger.controller.specs_manager import SwaggerSpecsManager
12+
from swagger.model.specs import TypeSpecResourceProvider
13+
from swagger.utils.source import SourceTypeEnum
14+
615
from utils.config import Config
16+
from utils.exceptions import InvalidAPIUsage
717

8-
logger = logging.getLogger('backend')
18+
logger = logging.getLogger('aaz')
919

1020
bp = Blueprint('cli-cmds', __name__, url_prefix='/CLI/CMDs', cli_group="cli")
1121
bp.cli.short_help = "Manage aaz commands in azure-cli and azure-cli-extensions."
@@ -39,9 +49,6 @@
3949
help="Name of the module in azure-cli or the extension in azure-cli-extensions"
4050
)
4151
def regenerate_code(extension_or_module_name, cli_path=None, cli_extension_path=None):
42-
from utils.config import Config
43-
from utils.exceptions import InvalidAPIUsage
44-
from cli.controller.az_module_manager import AzExtensionManager, AzMainManager
4552
if not cli_path and not cli_extension_path:
4653
logger.error("Please provide `--cli-path` or `--cli-extension-path`")
4754
sys.exit(1)
@@ -129,11 +136,6 @@ def regenerate_code(extension_or_module_name, cli_path=None, cli_extension_path=
129136
default=Config.CLI_DEFAULT_PROFILE,
130137
)
131138
def generate_by_swagger_tag(profile, swagger_tag, extension_or_module_name, cli_path=None, cli_extension_path=None):
132-
from utils.config import Config
133-
from utils.exceptions import InvalidAPIUsage
134-
from cli.controller.az_module_manager import AzExtensionManager, AzMainManager
135-
from swagger.controller.specs_manager import SwaggerSpecsManager
136-
from command.controller.specs_manager import AAZSpecsManager
137139
if not Config.DEFAULT_SWAGGER_MODULE:
138140
Config.DEFAULT_SWAGGER_MODULE = "__MODULE__"
139141

@@ -197,6 +199,189 @@ def generate_by_swagger_tag(profile, swagger_tag, extension_or_module_name, cli_
197199
sys.exit(1)
198200

199201

202+
@bp.cli.command("generate", short_help="Generate code effortlessly. If the result isn't what you expected, use the UI to fine-tune it.")
203+
@click.option(
204+
"--cli-path", '-c',
205+
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
206+
callback=Config.validate_and_setup_cli_path,
207+
help="The local path of azure-cli repo. Only required when generate code to azure-cli repo."
208+
)
209+
@click.option(
210+
"--cli-extension-path", '-e',
211+
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
212+
callback=Config.validate_and_setup_cli_extension_path,
213+
help="The local path of azure-cli-extension repo. Only required when generate code to azure-cli-extension repo."
214+
)
215+
@click.option(
216+
"--swagger-path", '-s',
217+
type=click.Path(file_okay=False, dir_okay=True, readable=True, resolve_path=True),
218+
default=Config.SWAGGER_PATH,
219+
callback=Config.validate_and_setup_swagger_path,
220+
expose_value=False,
221+
help="The local path of azure-rest-api-specs repo. Official repo is https://github.com/Azure/azure-rest-api-specs"
222+
)
223+
@click.option(
224+
"--aaz-path", '-a',
225+
type=click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True),
226+
default=Config.AAZ_PATH,
227+
required=not Config.AAZ_PATH,
228+
callback=Config.validate_and_setup_aaz_path,
229+
expose_value=False,
230+
help="The local path of aaz repo."
231+
)
232+
@click.option(
233+
"--spec",
234+
required=True,
235+
help="Folder name of the specification source."
236+
)
237+
@click.option(
238+
"--module",
239+
required=True,
240+
help="Module name of the CLI extension target."
241+
)
242+
def generate(spec, module, cli_path=None, cli_extension_path=None):
243+
def _collect_resources(spec):
244+
module_manager = SwaggerSpecsManager().get_module_manager(Config.DEFAULT_PLANE, spec)
245+
rps = module_manager.get_resource_providers()
246+
247+
results = {}
248+
for r in rps:
249+
if isinstance(r, TypeSpecResourceProvider):
250+
continue
251+
252+
rp = module_manager.get_openapi_resource_provider(r.name)
253+
tag = r.default_tag
254+
255+
resource_map = rp.get_resource_map_by_tag(tag)
256+
if not resource_map:
257+
raise InvalidAPIUsage(f"Tag `{tag}` is not exist.")
258+
259+
results[rp.name] = (resource_map, tag)
260+
261+
return results
262+
263+
def _normalize_resource_map(resource_map, tag):
264+
# covert resource_map to {version: [resources]}
265+
version_resource_map = {}
266+
for resource_id, version_map in resource_map.items():
267+
v_list = list(version_map)
268+
if len(v_list) > 1:
269+
raise InvalidAPIUsage(
270+
f"Tag `{tag}` contains multiple api versions of one resource",
271+
payload={"Resource": resource_id, "versions": v_list},
272+
)
273+
274+
v = v_list[0]
275+
version_resource_map.setdefault(v, []).append({"id": resource_id})
276+
277+
return version_resource_map
278+
279+
def to_aaz():
280+
results = _collect_resources(spec)
281+
282+
for rp_name, (resource_map, tag) in results.items():
283+
version_resource_map = _normalize_resource_map(resource_map, tag)
284+
285+
ws = WorkspaceManager.new(
286+
name=spec,
287+
plane=Config.DEFAULT_PLANE,
288+
folder=WorkspaceManager.IN_MEMORY,
289+
mod_names=spec,
290+
resource_provider=rp_name,
291+
swagger_manager=SwaggerSpecsManager(),
292+
aaz_manager=AAZSpecsManager(),
293+
source=SourceTypeEnum.OpenAPI,
294+
)
295+
for version, resources in version_resource_map.items():
296+
ws.add_new_resources_by_swagger(mod_names=spec, version=version, resources=resources)
297+
298+
# complete default help
299+
for node in ws.iter_command_tree_nodes():
300+
node.help = node.help or CMDHelp()
301+
if not node.help.short:
302+
node.help.short = f"Manage {node.names[-1]}."
303+
304+
for leaf in ws.iter_command_tree_leaves():
305+
leaf.help = leaf.help or CMDHelp()
306+
if not leaf.help.short:
307+
n = leaf.names[-1]
308+
leaf.help.short = f"{n.capitalize()} {leaf.names[-2]}"
309+
310+
cfg_editor = ws.load_cfg_editor_by_command(leaf)
311+
command = cfg_editor.find_command(*leaf.names)
312+
leaf.examples = ws.generate_examples_by_swagger(leaf, command)
313+
314+
if not ws.is_in_memory:
315+
ws.save()
316+
317+
ws.generate_to_aaz()
318+
319+
def to_cli():
320+
results = _collect_resources(spec)
321+
322+
commands_map = {}
323+
for _, (resource_map, tag) in results.items():
324+
for resource_id, version_map in resource_map.items():
325+
v_list = list(version_map)
326+
if len(v_list) > 1:
327+
raise InvalidAPIUsage(
328+
f"Tag `{tag}` contains multiple api versions of one resource",
329+
payload={"Resource": resource_id, "versions": v_list},
330+
)
331+
332+
v = v_list[0]
333+
cfg_reader = AAZSpecsManager().load_resource_cfg_reader(Config.DEFAULT_PLANE, resource_id, v)
334+
if not cfg_reader:
335+
logger.error(f"Command models not exist in aaz for resource: {resource_id} version: {v}.")
336+
continue
337+
338+
for cmd_names, command in cfg_reader.iter_commands():
339+
key = tuple(cmd_names)
340+
if key in commands_map and commands_map[key] != command.version:
341+
raise ValueError(f"Multi version contained for command: {''.join(cmd_names)} versions: {commands_map[key]}, {command.version}")
342+
343+
commands_map[key] = command.version
344+
345+
if cli_path is not None:
346+
assert Config.CLI_PATH is not None
347+
manager = AzMainManager()
348+
349+
else: # generate ext module by default
350+
assert Config.CLI_EXTENSION_PATH is not None
351+
manager = AzExtensionManager()
352+
353+
if not manager.has_module(module):
354+
logger.info(f"Create cli module `{module}`.")
355+
manager.create_new_mod(module)
356+
357+
logger.info(f"Load cli module `{module}`.")
358+
ext = manager.load_module(module)
359+
360+
profile = _build_profile(Config.CLI_DEFAULT_PROFILE, commands_map)
361+
ext.profiles[profile.name] = profile
362+
363+
logger.info(f"Regenerate module `{module}`.")
364+
manager.update_module(module, ext.profiles)
365+
366+
if cli_path and cli_extension_path:
367+
logger.error("Please provide either `--cli-path` or `--cli-extension-path`.")
368+
raise sys.exit(1)
369+
370+
spec_path = os.path.join(Config.SWAGGER_PATH, "specification", spec)
371+
if not os.path.exists(spec_path) or not os.path.isdir(spec_path):
372+
raise ValueError(f"Cannot find the specification name under {Config.SWAGGER_PATH}.")
373+
374+
try:
375+
to_aaz()
376+
logger.info("✔ Finished generating AAZ model.")
377+
to_cli()
378+
logger.info("✔ Finished generating AAZ codes.")
379+
380+
except (InvalidAPIUsage, ValueError) as err:
381+
logger.error(err, exc_info=True)
382+
raise sys.exit(1)
383+
384+
200385
def _build_profile(profile_name, commands_map):
201386
from cli.model.view import CLIViewProfile, CLIViewCommand, CLIViewCommandGroup
202387
profile = CLIViewProfile({

src/aaz_dev/cli/controller/az_atomic_profile_builder.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from utils.plane import PlaneEnum
1818
from utils.case import to_camel_case, to_snake_case
1919

20-
logger = logging.getLogger('backend')
20+
logger = logging.getLogger('aaz')
2121

2222

2323
class AzAtomicProfileBuilder:
@@ -248,8 +248,8 @@ def _complete_command_wait_info(cls, command_group):
248248

249249
for rid, value in [*wait_cmd_rids.items()]:
250250
if "get_op" not in value:
251-
logger.error(f'Failed to support wait command for resource: '
252-
f'Get operation with provisioning state property does not exist: {rid}')
251+
logger.warning(f'Failed to support wait command for resource: '
252+
f'Get operation with provisioning state property does not exist: {rid}')
253253
del wait_cmd_rids[rid]
254254

255255
if not wait_cmd_rids:

src/aaz_dev/cli/controller/az_client_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from utils.plane import PlaneEnum
1212
from utils.case import to_camel_case
1313

14-
logger = logging.getLogger('backend')
14+
logger = logging.getLogger('aaz')
1515

1616

1717
class AzClientsGenerator:

src/aaz_dev/cli/controller/az_command_ctx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .az_arg_group_generator import AzArgClsGenerator
77
from .az_operation_generator import AzRequestClsGenerator, AzResponseClsGenerator
88

9-
logger = logging.getLogger('backend')
9+
logger = logging.getLogger('aaz')
1010

1111

1212
class AzCommandCtx:

src/aaz_dev/cli/controller/az_command_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .az_output_generator import AzOutputGenerator
1515
from .az_selector_generator import AzJsonSelectorGenerator
1616

17-
logger = logging.getLogger('backend')
17+
logger = logging.getLogger('aaz')
1818

1919

2020
class AzCommandGenerator:

src/aaz_dev/cli/controller/az_module_manager.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from utils.config import Config
1515
from collections import deque
1616

17-
logger = logging.getLogger('backend')
17+
logger = logging.getLogger('aaz')
1818

1919

2020
class AzModuleManager:
@@ -36,9 +36,8 @@ def get_aaz_path(self, mod_name):
3636
raise NotImplementedError()
3737

3838
def has_module(self, mod_name):
39-
mod_folder = self.get_mod_path(mod_name)
40-
if not os.path.exists(mod_folder):
41-
#print(f"Invalid module folder: cannot find modules in: '{mod_folder}'")
39+
mod_file = os.path.join(self.get_mod_path(mod_name), "setup.py")
40+
if not os.path.exists(mod_file):
4241
return False
4342
return True
4443

src/aaz_dev/command/api/_cmds.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from swagger.utils.source import SourceTypeEnum
1414
from utils.config import Config
1515

16-
logger = logging.getLogger('backend')
16+
logger = logging.getLogger('aaz')
1717

1818
bp = Blueprint('aaz-cmds', __name__, url_prefix='/AAZ/CMDs', cli_group="command-model")
1919
bp.cli.short_help = "Manage command models in aaz."

src/aaz_dev/command/controller/specs_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .cfg_validator import CfgValidator
1717
from .command_tree import CMDSpecsPartialCommandTree
1818

19-
logger = logging.getLogger('backend')
19+
logger = logging.getLogger('aaz')
2020

2121

2222
class AAZSpecsManager:

src/aaz_dev/command/controller/workspace_cfg_editor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .cfg_reader import CfgReader
1111
from .workspace_helper import ArgumentUpdateMixin
1212

13-
logger = logging.getLogger('backend')
13+
logger = logging.getLogger('aaz')
1414

1515

1616
class WorkspaceCfgEditor(CfgReader, ArgumentUpdateMixin):

src/aaz_dev/command/controller/workspace_client_cfg_editor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
CMDClientEndpointsByHttpOperation, CMDClientEndpoints,
1010
CMDClientEndpointCloudMetadataTemplate)
1111

12-
logger = logging.getLogger('backend')
12+
logger = logging.getLogger('aaz')
1313

1414

1515
class WorkspaceClientCfgEditor(ClientCfgReader, ArgumentUpdateMixin):

0 commit comments

Comments
 (0)