Skip to content

Commit

Permalink
Merge pull request #361 from Azure/example-generation
Browse files Browse the repository at this point in the history
support example generation
  • Loading branch information
necusjz authored May 14, 2024
2 parents d443b6b + caa21a3 commit 18f7c95
Show file tree
Hide file tree
Showing 13 changed files with 893 additions and 62 deletions.
65 changes: 65 additions & 0 deletions src/aaz_dev/command/api/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,71 @@ def editor_workspace_command_tree_node_rename(name, node_names):
return jsonify(result)


@bp.route("/Workspaces/<name>/CommandTree/Nodes/<names_path:node_names>/Leaves/<name:leaf_name>/GenerateExamples",
methods=["POST"])
def editor_workspace_generate_examples(name, node_names, leaf_name):
if node_names[0] != WorkspaceManager.COMMAND_TREE_ROOT_NAME:
raise exceptions.ResourceNotFind("Command not exist.")

manager = WorkspaceManager(name)
manager.load()

node_names = node_names[1:]
leaf = manager.find_command_tree_leaf(*node_names, leaf_name)
if not leaf:
raise exceptions.ResourceNotFind("Command not exist.")

data = request.get_json()
source = data.get("source", None)

cfg_editor = manager.load_cfg_editor_by_command(leaf)
command = cfg_editor.find_command(*leaf.names)
if not command:
raise exceptions.ResourceNotFind("Command not exist.")

if source == "swagger":
examples = manager.generate_examples_by_swagger(leaf, command)
result = [example.to_primitive() for example in examples]
else:
raise exceptions.InvalidAPIUsage("Invalid request.")

return jsonify(result)


@bp.route("/Workspaces/<name>/CommandTree/Nodes/<names_path:node_names>/Leaves/<name:leaf_name>/Examples",
methods=["PATCH"])
def editor_workspace_examples(name, node_names, leaf_name):
if node_names[0] != WorkspaceManager.COMMAND_TREE_ROOT_NAME:
raise exceptions.ResourceNotFind("Command not exist")
node_names = node_names[1:]

manager = WorkspaceManager(name)
manager.load()
leaf = manager.find_command_tree_leaf(*node_names, leaf_name)
if not leaf:
raise exceptions.ResourceNotFind("Command not exist")

data = request.get_json()
if 'examples' in data:
leaf = manager.update_command_tree_leaf_examples(*leaf.names, examples=data['examples'])
cfg_editor = manager.load_cfg_editor_by_command(leaf)

command = cfg_editor.find_command(*leaf.names)
result = command.to_primitive()
manager.save()

del result['name']
result.update({
'names': leaf.names,
'help': leaf.help.to_primitive(),
'stage': leaf.stage,
})
if leaf.examples:
result['examples'] = [e.to_primitive() for e in leaf.examples]

return jsonify(result)


@bp.route("/Workspaces/<name>/CommandTree/Nodes/<names_path:node_names>/Leaves/<name:leaf_name>",
methods=("GET", "PATCH"))
def editor_workspace_command(name, node_names, leaf_name):
Expand Down
22 changes: 22 additions & 0 deletions src/aaz_dev/command/controller/shorthand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
def serialize(obj):
def dfs(obj):
if isinstance(obj, dict):
return "{" + ",".join(f'{k}:{dfs(v)}' for k, v in obj.items()) + "}"

elif isinstance(obj, list):
return "[" + ",".join(dfs(i) for i in obj) + "]"

else:
if obj is None:
return "null" # replace None by null

escaped = str(obj)
escaped = escaped.replace("'", "'/") # use '/ to input '

# with space, null/help expressions or other special characters
if any(char in escaped for char in (" ", "null", "??", ":", ",", "{", "}", "[", "]")):
escaped = "'" + escaped + "'"

return repr(escaped)[1:-1] # ignore quotes

return '"' + dfs(obj) + '"' if isinstance(obj, dict) or isinstance(obj, list) else obj
62 changes: 60 additions & 2 deletions src/aaz_dev/command/controller/workspace_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import shutil
from datetime import datetime

from command.model.configuration import CMDHelp, CMDResource, CMDCommandExample, CMDArg, CMDCommand, \
CMDBuildInVariants, CMDHttpOperation
from command.model.editor import CMDEditorWorkspace, CMDCommandTreeNode, CMDCommandTreeLeaf
from swagger.controller.command_generator import CommandGenerator
from swagger.controller.example_generator import ExampleGenerator
from swagger.controller.specs_manager import SwaggerSpecsManager
from swagger.utils.exceptions import InvalidSwaggerValueError
from utils import exceptions
Expand All @@ -14,7 +17,6 @@
from .specs_manager import AAZSpecsManager
from .workspace_cfg_editor import WorkspaceCfgEditor, build_endpoint_selector_for_client_config
from .workspace_client_cfg_editor import WorkspaceClientCfgEditor
from command.model.configuration import CMDHelp, CMDResource, CMDCommandExample, CMDArg, CMDCommand, CMDBuildInVariants

logger = logging.getLogger('backend')

Expand Down Expand Up @@ -87,6 +89,7 @@ def __init__(self, name, folder=None, aaz_manager=None, swagger_manager=None):
self._aaz_specs = aaz_manager
self._swagger_specs = swagger_manager
self._swagger_command_generator = None
self._swagger_example_generator = None

@property
def is_in_memory(self):
Expand All @@ -110,6 +113,13 @@ def swagger_command_generator(self):
self._swagger_command_generator = CommandGenerator()
return self._swagger_command_generator

@property
def swagger_example_generator(self):
if not self._swagger_example_generator:
self._swagger_example_generator = ExampleGenerator()

return self._swagger_example_generator

def load(self):
assert not self.is_in_memory
# TODO: handle exception
Expand Down Expand Up @@ -470,6 +480,54 @@ def update_command_tree_leaf_examples(self, *leaf_names, examples):
leaf.examples.append(example)
return leaf

def generate_examples_by_swagger(self, leaf, command):
def is_ready_to_skip(operations):
if not operations:
return True

if len(operations) == 1 and isinstance(operations[0], CMDHttpOperation):
return False

# skip when there is more than one operation and not all operations are get requests
for op in operations:
if not isinstance(op, CMDHttpOperation) or op.http.request.method != "get":
return True

return False

if is_ready_to_skip(command.operations):
return []

return self.generate_operations_examples_by_swagger(
command,
" ".join(leaf.names)
)

def generate_operations_examples_by_swagger(self, command, cmd_name):
root = self.find_command_tree_node()
assert root

# convert cmd resource to swagger resource
swagger_resources = []
for resource in command.resources:
swagger_resources.append(self.swagger_specs.get_module_manager(
plane=self.ws.plane,
mod_names=self.ws.mod_names
).get_resource_in_version(resource["id"], resource["version"], resource.rp_name))

# load swagger resource
cmd_operation_ids = {op.operation_id: op for op in command.operations}
self.swagger_example_generator.load_examples(swagger_resources)

examples = self.swagger_example_generator.create_draft_examples_by_swagger(
swagger_resources,
command,
cmd_operation_ids,
cmd_name
)

return examples

def rename_command_tree_node(self, *node_names, new_node_names):
new_name = ' '.join(new_node_names)
if not new_name:
Expand Down Expand Up @@ -1101,7 +1159,7 @@ def load_client_cfg_editor(self, reload=False):
logger.error(
f"load workspace client cfg failed: {e}: {self.name}")
return None

def compare_client_cfg_with_spec(self):
""" Check whether the client configuration version in workspace is later than the aaz specs one. """
# compare client configuration from aaz specs
Expand Down
145 changes: 145 additions & 0 deletions src/aaz_dev/command/tests/api_tests/test_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2593,3 +2593,148 @@ def test_dataplane_attestation(self, ws_name):
})
self.assertTrue(rv.status_code == 400)
self.assertEqual(rv.json['message'], "Not support remain index ['invalid']")

@workspace_name("test_workspace_generate_examples_array")
def test_workspace_generate_examples_array(self, ws_name):
module = "eventhub"
api_version = "2021-11-01"

with self.app.test_client() as c:
rv = c.post(f"/AAZ/Editor/Workspaces", json={
"name": ws_name,
"plane": PlaneEnum.Mgmt,
"modNames": module,
"resourceProvider": "Microsoft.EventHub"
})
self.assertTrue(rv.status_code == 200)

ws_url = rv.get_json()["url"]
resource_id = swagger_resource_path_to_resource_id("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/networkRuleSets/default")

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger",
json={
"module": module,
"version": api_version,
"resources": [
{
"id": resource_id,
"options": {
"aaz_version": None
}
}
]
}
)
self.assertTrue(rv.status_code == 200)

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/event-hub/namespace/network-rule-set/default/Leaves/create/Arguments/$parameters.properties.virtualNetworkRules[].subnet/Flatten",
json={
"subArgsOptions": {
"$parameters.properties.virtualNetworkRules[].subnet.id": ["id"]
}
}
)
self.assertTrue(rv.status_code == 200)

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/event-hub/namespace/network-rule-set/default/Leaves/create/GenerateExamples",
json={
"source": "swagger"
}
)
self.assertTrue(rv.status_code == 200)

@workspace_name("test_workspace_generate_examples_dict")
def test_workspace_generate_examples_dict(self, ws_name):
module = "eventhub"
api_version = "2021-11-01"

with self.app.test_client() as c:
rv = c.post(f"/AAZ/Editor/Workspaces", json={
"name": ws_name,
"plane": PlaneEnum.Mgmt,
"modNames": module,
"resourceProvider": "Microsoft.EventHub"
})
self.assertTrue(rv.status_code == 200)

ws_url = rv.get_json()["url"]
resource_id = swagger_resource_path_to_resource_id("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/clusters/{clusterName}")

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger",
json={
"module": module,
"version": api_version,
"resources": [
{
"id": resource_id,
"options": {
"aaz_version": None
}
}
]
}
)
self.assertTrue(rv.status_code == 200)

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/event-hub/cluster/Leaves/create/GenerateExamples",
json={
"source": "swagger"
}
)
self.assertTrue(rv.status_code == 200)

@workspace_name("test_workspace_generate_examples_poly")
def test_workspace_generate_examples_poly(self, ws_name):
module = "monitor"
api_version = "2018-03-01"

with self.app.test_client() as c:
rv = c.post(
f"/AAZ/Editor/Workspaces",
json={
"name": ws_name,
"plane": PlaneEnum.Mgmt,
"modNames": module,
"resourceProvider": "Microsoft.Insights"
}
)
self.assertTrue(rv.status_code == 200)

ws_url = rv.get_json()["url"]

resource_id = swagger_resource_path_to_resource_id("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Insights/metricAlerts/{ruleName}")

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger",
json={
"module": module,
"version": api_version,
"resources": [
{
"id": resource_id,
"options": {
"aaz_version": None
}
}
]
}
)
self.assertTrue(rv.status_code == 200)

rv = c.post(
f"{ws_url}/CommandTree/Nodes/aaz/insights/metric-alert/Leaves/create/GenerateExamples",
json={
"source": "swagger"
}
)
self.assertTrue(rv.status_code == 200)

self.assertTrue(
"microsoft-azure-monitor-multiple-resource-multiple-metric-criteria" in rv.json[1]["commands"][0]
)
self.assertTrue("static-threshold-criterion" in rv.json[1]["commands"][0])
36 changes: 36 additions & 0 deletions src/aaz_dev/command/tests/editor_tests/test_serialize_shorthand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from command.controller.shorthand import serialize


def test_with_nesting():
obj = {
"microsoft-azure-monitor-single-resource-multiple-metric-criteria": {
"all-of": [{
"name": "High_CPU_80",
"metric-name": "Time",
"dimensions": [],
"operator": "GreaterThan",
"threshold": 80.5,
"time-aggregation": "Average"
}]
}
}

assert serialize(obj) == '"{microsoft-azure-monitor-single-resource-multiple-metric-criteria:{all-of:[{name:High_CPU_80,metric-name:Time,dimensions:[],operator:GreaterThan,threshold:80.5,time-aggregation:Average}]}}"'


def test_with_single_quote():
obj = {
"name": "monitor's"
}

assert serialize(obj) == '"{name:monitor\'/s}"'


def test_with_special_characters():
obj = {
"name": "monitor metric",
"dimensions": None,
"data": "{a: [1, 2]}"
}

assert serialize(obj) == '"{name:\'monitor metric\',dimensions:null,data:\'{a: [1, 2]}\'}"'
Loading

0 comments on commit 18f7c95

Please sign in to comment.