diff --git a/src/aaz_dev/cli/controller/az_arg_group_generator.py b/src/aaz_dev/cli/controller/az_arg_group_generator.py index cdd336cb..62057b9e 100644 --- a/src/aaz_dev/cli/controller/az_arg_group_generator.py +++ b/src/aaz_dev/cli/controller/az_arg_group_generator.py @@ -294,6 +294,10 @@ def render_arg_base(arg, cmd_ctx, arg_kwargs=None): if isinstance(arg, CMDAnyTypeArgBase): arg_type = "AAZAnyTypeArg" + elif isinstance(arg, CMDByteArgBase): + raise NotImplementedError() + elif isinstance(arg, CMDBinaryArgBase): + arg_type = "AAZFileUploadArg" elif isinstance(arg, CMDStringArgBase): arg_type = "AAZStrArg" enum_kwargs = parse_arg_enum(arg.enum) @@ -347,10 +351,6 @@ def render_arg_base(arg, cmd_ctx, arg_kwargs=None): "resource_group_arg": resource_group_arg } } - elif isinstance(arg, CMDByteArgBase): - raise NotImplementedError() - elif isinstance(arg, CMDBinaryArgBase): - raise NotImplementedError() elif isinstance(arg, CMDDurationArgBase): arg_type = "AAZDurationArg" elif isinstance(arg, CMDDateArgBase): diff --git a/src/aaz_dev/cli/controller/az_operation_generator.py b/src/aaz_dev/cli/controller/az_operation_generator.py index bec6177a..f7908894 100644 --- a/src/aaz_dev/cli/controller/az_operation_generator.py +++ b/src/aaz_dev/cli/controller/az_operation_generator.py @@ -1,8 +1,8 @@ from command.model.configuration import ( - CMDHttpOperation, CMDHttpRequestJsonBody, CMDArraySchema, CMDInstanceUpdateOperation, CMDRequestJson, + CMDHttpOperation, CMDHttpRequestJsonBody, CMDArraySchema, CMDInstanceUpdateOperation, CMDRequestJson, CMDHttpRequestBinaryBody, CMDRequestBinary, CMDHttpResponseJsonBody, CMDObjectSchema, CMDSchema, CMDStringSchemaBase, CMDIntegerSchemaBase, CMDFloatSchemaBase, CMDBooleanSchemaBase, CMDObjectSchemaBase, CMDArraySchemaBase, CMDClsSchemaBase, CMDJsonInstanceUpdateAction, - CMDObjectSchemaDiscriminator, CMDSchemaEnum, CMDJsonInstanceCreateAction, CMDJsonInstanceDeleteAction, + CMDObjectSchemaDiscriminator, CMDSchemaEnum, CMDJsonInstanceCreateAction, CMDJsonInstanceDeleteAction, CMDBinarySchema, CMDInstanceCreateOperation, CMDInstanceDeleteOperation, CMDClientEndpointsByTemplate, CMDIdentityObjectSchemaBase, CMDAnyTypeSchemaBase) from utils import exceptions from utils.case import to_snake_case @@ -114,10 +114,14 @@ def __init__(self, name, cmd_ctx, operation, client_endpoints): self.content = None self.form_content = None self.stream_content = None + self.content_as_binary = None if self._operation.http.request.body: body = self._operation.http.request.body if isinstance(body, CMDHttpRequestJsonBody): self.content = AzHttpRequestContentGenerator(self._cmd_ctx, body) + elif isinstance(body, CMDHttpRequestBinaryBody): + self.content_as_binary = True + self.content = AzHttpRequestContentBytesGenerator(self._cmd_ctx, body) else: raise NotImplementedError() @@ -260,6 +264,19 @@ def header_parameters(self): True, {} ]) + elif isinstance(body, CMDHttpRequestBinaryBody): + parameters.append([ + "Content-Type", + "application/octet-stream", + True, + {} + ]) + parameters.append([ + "Content-Length", + "self.ctx.file_length", + False, + {} + ]) if self.success_responses: for response in self.success_responses: if response._response.body is not None and isinstance(response._response.body, CMDHttpResponseJsonBody): @@ -510,6 +527,23 @@ def iter_scopes(self): for scopes in _iter_request_scopes_by_schema_base(self.schema, self.BUILDER_NAME, None, arg_key, self._cmd_ctx): yield scopes +class AzHttpRequestContentBytesGenerator: + + def __init__(self, cmd_ctx, body): + self._cmd_ctx = cmd_ctx + assert isinstance(body.bytes, CMDRequestBinary) + self._bodycontent = body.bytes + self.ref = None + if self._bodycontent.ref: + self.ref, is_selector = self._cmd_ctx.get_variant(self._bodycontent.ref) + assert not is_selector + self.arg_key = "self.ctx.args" + if self.ref is None: + assert isinstance(self._bodycontent.schema, CMDSchema) + if self._bodycontent.schema.arg: + self.arg_key, hide = self._cmd_ctx.get_argument(self._bodycontent.schema.arg) + assert not hide + class AzHttpResponseGenerator: @@ -990,6 +1024,8 @@ def render_schema_base(schema, cls_map, schema_kwargs=None): schema_type = "AAZFloatType" elif isinstance(schema, CMDIdentityObjectSchemaBase): schema_type = "AAZIdentityObjectType" + elif isinstance(schema, CMDBinarySchema): + schema_type = "AAZFileUploadType" elif isinstance(schema, CMDObjectSchemaBase): if schema.props or schema.discriminators: schema_type = "AAZObjectType" diff --git a/src/aaz_dev/cli/templates/aaz/command/_cmd.py.j2 b/src/aaz_dev/cli/templates/aaz/command/_cmd.py.j2 index eaf2520d..e77d2b34 100644 --- a/src/aaz_dev/cli/templates/aaz/command/_cmd.py.j2 +++ b/src/aaz_dev/cli/templates/aaz/command/_cmd.py.j2 @@ -236,11 +236,21 @@ class {{ leaf.cls_name }}( @register_callback def pre_operations(self): + {%- if leaf.http_operations[0].content is not none and leaf.http_operations[0].content_as_binary is not none %} + self.ctx.file_content, self.ctx.file_handler, self.ctx.file_length = {{ leaf.http_operations[0].content.arg_key }}.to_serialized_data() + {%- else %} pass + {%- endif %} @register_callback def post_operations(self): + {%- if leaf.http_operations[0].content is not none and leaf.http_operations[0].content_as_binary is not none %} + if self.ctx.file_handler is not None: + self.ctx.file_handler.close() + self.ctx.file_handler = None + {%- else %} pass + {%- endif %} {%- for op in leaf.operations %} {%- if op.name in ("pre_instance_update", "post_instance_update", "post_instance_create") %} @@ -485,7 +495,7 @@ class {{ leaf.cls_name }}( return parameters {%- endif %} - {%- if op.content is not none %} + {%- if op.content is not none and op.content_as_binary is none %} @property def content(self): @@ -548,6 +558,13 @@ class {{ leaf.cls_name }}( return self.serialize_content({{ op.content.VALUE_NAME }}) {%- endif %} + {%- if op.content is not none and op.content_as_binary is not none %} + + @property + def content(self): + return self.ctx.file_content + {%- endif %} + {%- if op.form_content is not none %} @property @@ -556,6 +573,13 @@ class {{ leaf.cls_name }}( return None {%- endif %} + {%- if op.content is not none and op.content_as_binary is not none %} + + @property + def stream_content(self): + return self.ctx.file_handler + {%- endif %} + {%- if op.stream_content is not none %} @property diff --git a/src/aaz_dev/command/model/configuration/__init__.py b/src/aaz_dev/command/model/configuration/__init__.py index 5c7e23fe..3cee8c48 100644 --- a/src/aaz_dev/command/model/configuration/__init__.py +++ b/src/aaz_dev/command/model/configuration/__init__.py @@ -35,7 +35,7 @@ CMDConditionAndOperator, CMDConditionOrOperator, CMDConditionNotOperator, CMDConditionHasValueOperator, \ CMDCondition from ._configuration import CMDConfiguration -from ._content import CMDRequestJson, CMDResponseJson +from ._content import CMDRequestJson, CMDResponseJson, CMDRequestBinary from ._example import CMDCommandExample from ._fields import CMDBooleanField, CMDStageField, CMDVariantField, CMDClassField, \ CMDPrimitiveField, CMDRegularExpressionField, CMDVersionField, CMDResourceIdField, CMDCommandNameField, \ @@ -47,7 +47,7 @@ CMDHttpRequest, \ CMDHttpResponseHeaderItem, CMDHttpResponseHeader, CMDHttpResponse, \ CMDHttpAction -from ._http_request_body import CMDHttpRequestBody, CMDHttpRequestJsonBody +from ._http_request_body import CMDHttpRequestBody, CMDHttpRequestJsonBody, CMDHttpRequestBinaryBody from ._http_response_body import CMDHttpResponseBody, CMDHttpResponseJsonBody from ._instance_create import CMDInstanceCreateAction, CMDJsonInstanceCreateAction from ._instance_delete import CMDInstanceDeleteAction, CMDJsonInstanceDeleteAction diff --git a/src/aaz_dev/command/model/configuration/_arg.py b/src/aaz_dev/command/model/configuration/_arg.py index fe409f13..38e7d417 100644 --- a/src/aaz_dev/command/model/configuration/_arg.py +++ b/src/aaz_dev/command/model/configuration/_arg.py @@ -384,20 +384,20 @@ def _reformat(self, **kwargs): # byte: base64 encoded characters -class CMDByteArgBase(CMDStringArgBase): +class CMDByteArgBase(CMDArgBase): TYPE_VALUE = "byte" -class CMDByteArg(CMDByteArgBase, CMDStringArg): +class CMDByteArg(CMDByteArgBase, CMDArg): pass # binary: any sequence of octets -class CMDBinaryArgBase(CMDStringArgBase): +class CMDBinaryArgBase(CMDArgBase): TYPE_VALUE = "binary" -class CMDBinaryArg(CMDBinaryArgBase, CMDStringArg): +class CMDBinaryArg(CMDBinaryArgBase, CMDArg): pass diff --git a/src/aaz_dev/command/model/configuration/_arg_builder.py b/src/aaz_dev/command/model/configuration/_arg_builder.py index e20e592d..5439cc44 100644 --- a/src/aaz_dev/command/model/configuration/_arg_builder.py +++ b/src/aaz_dev/command/model/configuration/_arg_builder.py @@ -7,7 +7,7 @@ from ._format import CMDFormat from ._schema import CMDObjectSchema, CMDSchema, CMDSchemaBase, CMDObjectSchemaBase, CMDObjectSchemaDiscriminator, \ CMDArraySchemaBase, CMDArraySchema, CMDObjectSchemaAdditionalProperties, CMDResourceIdSchema, \ - CMDResourceLocationSchemaBase, CMDPasswordSchema, CMDBooleanSchemaBase + CMDResourceLocationSchemaBase, CMDPasswordSchema, CMDBooleanSchemaBase, CMDBinarySchema from ..configuration._schema import CMDIdentityObjectSchema, CMDStringSchemaBase, \ CMDStringSchema @@ -381,6 +381,8 @@ def get_options(self): if self.schema.action is not None and self.schema.name in ["userAssigned", "systemAssigned"]: return [opt_name, "mi-" + opt_name] + if isinstance(self.schema, CMDBinarySchema): + return [opt_name, "body-file", "body-file-path"] else: raise NotImplementedError() return [opt_name, ] diff --git a/src/aaz_dev/command/model/configuration/_content.py b/src/aaz_dev/command/model/configuration/_content.py index ec36df5e..db4a6fbc 100644 --- a/src/aaz_dev/command/model/configuration/_content.py +++ b/src/aaz_dev/command/model/configuration/_content.py @@ -4,7 +4,7 @@ from ._arg_builder import CMDArgBuilder from ._fields import CMDVariantField from ._schema import CMDSchemaBaseField, CMDSchema, CMDClsSchema, CMDClsSchemaBase, \ - CMDObjectSchemaBase, CMDArraySchemaBase, CMDObjectSchemaDiscriminator + CMDObjectSchemaBase, CMDArraySchemaBase, CMDObjectSchemaDiscriminator, CMDBinarySchema from ._utils import CMDDiffLevelEnum @@ -63,6 +63,58 @@ def reformat(self, schema_cls_map, **kwargs): def register_cls(self, cls_register_map, **kwargs): _iter_over_schema_for_cls_register(self.schema, cls_register_map) +class CMDRequestBinary(Model): + """Used for Request Binary Body""" + + ref = CMDVariantField() + + schema = PolyModelType(CMDBinarySchema, allow_subclasses=False) + + class Options: + serialize_when_none = False + + def generate_args(self, ref_args, var_prefix=None, is_update_action=False): + if not self.schema: + return [] + assert isinstance(self.schema, CMDBinarySchema) + builder = CMDArgBuilder.new_builder( + schema=self.schema, + ref_args=ref_args, + var_prefix=var_prefix, + is_update_action=is_update_action + ) + args = builder.get_args() + return args + + def diff(self, old, level): + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if (self.ref is not None) != (old.ref is not None): + diff["ref"] = f"{old.ref} != {self.ref}" + schema_diff = self.schema.diff(old.schema, level) + if schema_diff: + diff["schema"] = schema_diff + + if level >= CMDDiffLevelEnum.Associate: + if self.ref != old.ref: + diff["ref"] = f"{old.ref} != {self.ref}" + return diff + + def reformat(self, schema_cls_map, **kwargs): + if self.schema: + if getattr(self.schema, 'cls', None): + if not schema_cls_map.get(self.schema.cls, None): + schema_cls_map[self.schema.cls] = self.schema + else: + # replace by CMDClsSchema + self.schema = CMDClsSchema.build_from_schema(self.schema, schema_cls_map[self.schema.cls]) + + _iter_over_schema(self.schema, schema_cls_map) + + self.schema.reformat(**kwargs) + + def register_cls(self, cls_register_map, **kwargs): + _iter_over_schema_for_cls_register(self.schema, cls_register_map) class CMDResponseJson(Model): # properties as tags diff --git a/src/aaz_dev/command/model/configuration/_http_request_body.py b/src/aaz_dev/command/model/configuration/_http_request_body.py index 74df7e5d..6adaae36 100644 --- a/src/aaz_dev/command/model/configuration/_http_request_body.py +++ b/src/aaz_dev/command/model/configuration/_http_request_body.py @@ -1,7 +1,7 @@ from schematics.models import Model from schematics.types import ModelType -from ._content import CMDRequestJson +from ._content import CMDRequestJson, CMDRequestBinary class CMDHttpRequestBody(Model): @@ -50,3 +50,22 @@ def reformat(self, **kwargs): def register_cls(self, **kwargs): self.json.register_cls(**kwargs) + +class CMDHttpRequestBinaryBody(CMDHttpRequestBody): + POLYMORPHIC_KEY = "bytes" + + bytes = ModelType(CMDRequestBinary, required=True) + + def generate_args(self, ref_args, var_prefix=None): + return self.bytes.generate_args(ref_args=ref_args, var_prefix=var_prefix) + + def diff(self, old, level): + if not isinstance(old, self.__class__): + return f"Response type changed: '{type(old)}' != '{self.__class__}'" + return self.bytes.diff(old.bytes, level) + + def reformat(self, **kwargs): + self.bytes.reformat(**kwargs) + + def register_cls(self, **kwargs): + self.bytes.register_cls(**kwargs) diff --git a/src/aaz_dev/command/model/configuration/_schema.py b/src/aaz_dev/command/model/configuration/_schema.py index ab75ab7a..97cda46b 100644 --- a/src/aaz_dev/command/model/configuration/_schema.py +++ b/src/aaz_dev/command/model/configuration/_schema.py @@ -489,22 +489,32 @@ class CMDStringSchema(CMDStringSchemaBase, CMDSchema): # byte: base64 encoded characters -class CMDByteSchemaBase(CMDStringSchemaBase): +class CMDByteSchemaBase(CMDSchemaBase): TYPE_VALUE = "byte" ARG_TYPE = CMDByteArgBase -class CMDByteSchema(CMDByteSchemaBase, CMDStringSchema): +class CMDByteSchema(CMDByteSchemaBase, CMDSchema): ARG_TYPE = CMDByteArg # binary: any sequence of octets -class CMDBinarySchemaBase(CMDStringSchemaBase): +class CMDBinarySchemaBase(CMDSchemaBase): TYPE_VALUE = "binary" ARG_TYPE = CMDBinaryArgBase + name = StringType(required=True) + arg = CMDVariantField() + required = CMDBooleanField() + + description = CMDDescriptionField() + fmt = ModelType( + CMDStringFormat, + serialized_name='format', + deserialize_from='format' + ) -class CMDBinarySchema(CMDBinarySchemaBase, CMDStringSchema): +class CMDBinarySchema(CMDBinarySchemaBase, CMDSchema): ARG_TYPE = CMDBinaryArg diff --git a/src/aaz_dev/swagger/controller/command_generator.py b/src/aaz_dev/swagger/controller/command_generator.py index b5002626..99844b16 100644 --- a/src/aaz_dev/swagger/controller/command_generator.py +++ b/src/aaz_dev/swagger/controller/command_generator.py @@ -6,7 +6,7 @@ from command.model.configuration import CMDCommandGroup, CMDCommand, CMDHttpOperation, CMDHttpRequest, \ CMDSchemaDefault, CMDHttpResponseJsonBody, CMDArrayOutput, CMDJsonInstanceUpdateAction, \ CMDInstanceUpdateOperation, CMDRequestJson, DEFAULT_CONFIRMATION_PROMPT, CMDClsSchemaBase, CMDHttpResponse, \ - CMDResponseJson, CMDResource + CMDResponseJson, CMDResource, CMDHttpRequestBinaryBody from swagger.model.schema.cmd_builder import CMDBuilder from swagger.model.schema.fields import MutabilityEnum from swagger.model.schema.path_item import PathItem @@ -404,10 +404,13 @@ def create_draft_command_group(self, resource, delete_command.confirmation = DEFAULT_CONFIRMATION_PROMPT # add confirmation for delete command by default command_group.commands.append(delete_command) + skip_update = False if path_item.put is not None and 'put' in methods: cmd_builder = CMDBuilder(path=resource.path, method='put', mutability=MutabilityEnum.Create, parameterized_host=parameterized_host) op = self.generate_operation(cmd_builder, path_item, instance_var) + if isinstance(op.http.request.body, CMDHttpRequestBinaryBody): + skip_update = True create_command = self.generate_command(path_item, resource, instance_var, cmd_builder, op) command_group.commands.append(create_command) @@ -426,29 +429,30 @@ def create_draft_command_group(self, resource, command_group.commands.append(head_command) # update command - if update_by is None: - update_by_patch_command = None - update_by_generic_command = None - if path_item.patch is not None and 'patch' in methods: - cmd_builder = CMDBuilder(path=resource.path, method='patch', mutability=MutabilityEnum.Update, - parameterized_host=parameterized_host) - op = self.generate_operation(cmd_builder, path_item, instance_var) - update_by_patch_command = self.generate_command(path_item, resource, instance_var, cmd_builder, op) - if path_item.get is not None and path_item.put is not None and 'get' in methods and 'put' in methods: - cmd_builder = CMDBuilder(path=resource.path, - parameterized_host=parameterized_host) - get_op = self.generate_operation( - cmd_builder, path_item, instance_var, method='get', mutability=MutabilityEnum.Read) - put_op = self.generate_operation( - cmd_builder, path_item, instance_var, method='put', mutability=MutabilityEnum.Update) - update_by_generic_command = self.generate_generic_update_command(path_item, resource, instance_var, cmd_builder, get_op, put_op) - # generic update command first, patch update command after that - if update_by_generic_command: - command_group.commands.append(update_by_generic_command) - elif update_by_patch_command: - command_group.commands.append(update_by_patch_command) - else: - if update_by == 'GenericOnly': + if not skip_update: + if update_by is None: + update_by_patch_command = None + update_by_generic_command = None + if path_item.patch is not None and 'patch' in methods: + cmd_builder = CMDBuilder(path=resource.path, method='patch', mutability=MutabilityEnum.Update, + parameterized_host=parameterized_host) + op = self.generate_operation(cmd_builder, path_item, instance_var) + update_by_patch_command = self.generate_command(path_item, resource, instance_var, cmd_builder, op) + if path_item.get is not None and path_item.put is not None and 'get' in methods and 'put' in methods: + cmd_builder = CMDBuilder(path=resource.path, + parameterized_host=parameterized_host) + get_op = self.generate_operation( + cmd_builder, path_item, instance_var, method='get', mutability=MutabilityEnum.Read) + put_op = self.generate_operation( + cmd_builder, path_item, instance_var, method='put', mutability=MutabilityEnum.Update) + update_by_generic_command = self.generate_generic_update_command(path_item, resource, instance_var, + cmd_builder, get_op, put_op) + # generic update command first, patch update command after that + if update_by_generic_command: + command_group.commands.append(update_by_generic_command) + elif update_by_patch_command: + command_group.commands.append(update_by_patch_command) + elif update_by == 'GenericOnly': if path_item.get is None or path_item.put is None: raise exceptions.InvalidAPIUsage(f"Invalid update_by resource: resource needs to have 'get' and 'put' operations: '{resource}'") if 'get' not in methods or 'put' not in methods: diff --git a/src/aaz_dev/swagger/model/schema/operation.py b/src/aaz_dev/swagger/model/schema/operation.py index d41654a3..1468a26e 100644 --- a/src/aaz_dev/swagger/model/schema/operation.py +++ b/src/aaz_dev/swagger/model/schema/operation.py @@ -5,7 +5,7 @@ from schematics.types import StringType, ModelType, ListType, DictType, BooleanType, PolyModelType from command.model.configuration import CMDHttpOperation, CMDHttpAction, CMDHttpRequest, CMDHttpRequestPath, \ - CMDHttpRequestQuery, CMDHttpRequestHeader, CMDHttpRequestJsonBody, CMDRequestJson, CMDHttpOperationLongRunning + CMDHttpRequestQuery, CMDHttpRequestHeader, CMDHttpRequestJsonBody, CMDRequestJson, CMDRequestBinary, CMDHttpRequestBinaryBody, CMDHttpOperationLongRunning from swagger.utils import exceptions from swagger.utils.tools import swagger_resource_path_to_resource_id_template from .example_item import XmsExamplesField @@ -225,6 +225,9 @@ def to_cmd(self, builder, parent_parameters, host_path, **kwargs): if isinstance(model, CMDRequestJson): request.body = CMDHttpRequestJsonBody() request.body.json = model + elif isinstance(model, CMDRequestBinary): + request.body = CMDHttpRequestBinaryBody() + request.body.bytes = model else: raise NotImplementedError() diff --git a/src/aaz_dev/swagger/model/schema/parameter.py b/src/aaz_dev/swagger/model/schema/parameter.py index 9ff675e6..ec50cf6b 100644 --- a/src/aaz_dev/swagger/model/schema/parameter.py +++ b/src/aaz_dev/swagger/model/schema/parameter.py @@ -1,7 +1,7 @@ import logging from command.model.configuration import CMDRequestJson, CMDBooleanSchema, CMDStringSchema, CMDObjectSchema, \ - CMDArraySchema, CMDFloatSchema, CMDIntegerSchema + CMDArraySchema, CMDFloatSchema, CMDIntegerSchema, CMDBinarySchema, CMDRequestBinary from schematics.models import Model from schematics.types import StringType, BooleanType, ModelType, PolyModelType, BaseType from swagger.utils import exceptions @@ -205,7 +205,11 @@ def to_cmd(self, builder, **kwargs): msg=f"Request Body Parameter is None: {self.traces}" ) return None - if isinstance(v, ( + if isinstance(v, CMDBinarySchema): + model = CMDRequestBinary() + model.schema = v + v.required = self.required + elif isinstance(v, ( CMDStringSchema, CMDObjectSchema, CMDArraySchema,