diff --git a/src/azure-cli-core/azure/cli/core/aaz/__init__.py b/src/azure-cli-core/azure/cli/core/aaz/__init__.py index 3b41646a2a0..0f0181a185e 100644 --- a/src/azure-cli-core/azure/cli/core/aaz/__init__.py +++ b/src/azure-cli-core/azure/cli/core/aaz/__init__.py @@ -11,9 +11,10 @@ from ._arg import has_value, AAZArgumentsSchema, AAZArgEnum, AAZStrArg, AAZIntArg, AAZObjectArg, AAZDictArg, \ AAZFloatArg, AAZBaseArg, AAZBoolArg, AAZListArg, AAZResourceGroupNameArg, AAZResourceLocationArg, \ - AAZResourceIdArg, AAZSubscriptionIdArg + AAZResourceIdArg, AAZSubscriptionIdArg, AAZUuidArg, AAZDateArg, AAZTimeArg, AAZDateTimeArg, AAZDurationArg from ._arg_fmt import AAZStrArgFormat, AAZIntArgFormat, AAZFloatArgFormat, AAZBoolArgFormat, AAZObjectArgFormat, \ - AAZDictArgFormat, AAZListArgFormat, AAZResourceLocationArgFormat, AAZResourceIdArgFormat, AAZSubscriptionIdArgFormat + AAZDictArgFormat, AAZListArgFormat, AAZResourceLocationArgFormat, AAZResourceIdArgFormat, \ + AAZSubscriptionIdArgFormat, AAZUuidFormat, AAZDateFormat, AAZTimeFormat, AAZDateTimeFormat, AAZDurationFormat from ._base import AAZValuePatch, AAZUndefined from ._command import AAZCommand, AAZWaitCommand, AAZCommandGroup, \ register_command, register_command_group, load_aaz_command_table diff --git a/src/azure-cli-core/azure/cli/core/aaz/_arg.py b/src/azure-cli-core/azure/cli/core/aaz/_arg.py index bda2983d08c..fda163f25aa 100644 --- a/src/azure-cli-core/azure/cli/core/aaz/_arg.py +++ b/src/azure-cli-core/azure/cli/core/aaz/_arg.py @@ -14,7 +14,8 @@ AAZSimpleType from ._field_value import AAZObject from ._arg_fmt import AAZObjectArgFormat, AAZListArgFormat, AAZDictArgFormat, AAZSubscriptionIdArgFormat, \ - AAZResourceLocationArgFormat, AAZResourceIdArgFormat + AAZResourceLocationArgFormat, AAZResourceIdArgFormat, AAZUuidFormat, AAZDateFormat, AAZTimeFormat, \ + AAZDateTimeFormat, AAZDurationFormat # pylint: disable=redefined-builtin, protected-access @@ -166,6 +167,61 @@ def _type_in_help(self): return "String" +class AAZDurationArg(AAZStrArg): + + def __init__(self, fmt=None, **kwargs): + fmt = fmt or AAZDurationFormat() + super().__init__(fmt=fmt, **kwargs) + + @property + def _type_in_help(self): + return "Duration" + + +class AAZDateArg(AAZStrArg): + + def __init__(self, fmt=None, **kwargs): + fmt = fmt or AAZDateFormat() + super().__init__(fmt=fmt, **kwargs) + + @property + def _type_in_help(self): + return "Date" + + +class AAZTimeArg(AAZStrArg): + + def __init__(self, fmt=None, **kwargs): + fmt = fmt or AAZTimeFormat() + super().__init__(fmt=fmt, **kwargs) + + @property + def _type_in_help(self): + return "Time" + + +class AAZDateTimeArg(AAZStrArg): + + def __init__(self, fmt=None, **kwargs): + fmt = fmt or AAZDateTimeFormat() + super().__init__(fmt=fmt, **kwargs) + + @property + def _type_in_help(self): + return "DateTime" + + +class AAZUuidArg(AAZStrArg): + + def __init__(self, fmt=None, **kwargs): + fmt = fmt or AAZUuidFormat() + super().__init__(fmt=fmt, **kwargs) + + @property + def _type_in_help(self): + return "GUID/UUID" + + class AAZIntArg(AAZSimpleTypeArg, AAZIntType): @property diff --git a/src/azure-cli-core/azure/cli/core/aaz/_arg_fmt.py b/src/azure-cli-core/azure/cli/core/aaz/_arg_fmt.py index 3f3c158186f..b222bd9689f 100644 --- a/src/azure-cli-core/azure/cli/core/aaz/_arg_fmt.py +++ b/src/azure-cli-core/azure/cli/core/aaz/_arg_fmt.py @@ -56,6 +56,220 @@ def __call__(self, ctx, value): return value +class AAZDurationFormat(AAZBaseArgFormat): + + def __call__(self, ctx, value): + assert isinstance(value, AAZSimpleValue) + data = value._data + if data == AAZUndefined or data is None or value._is_patch: + return value + + assert isinstance(data, str) + from msrest.serialization import Serializer + from isodate.isoerror import ISO8601Error + + try: + data = Serializer.serialize_duration(data.upper()) + except ISO8601Error: + try: + # parse '##DT##H##M##S + data = Serializer.serialize_duration(f'P0M{data.upper()}') + except ISO8601Error: + try: + # parse '##H##M##S' + data = Serializer.serialize_duration(f'PT{data.upper()}') + except ISO8601Error: + raise AAZInvalidArgValueError( + f"Invalid format: '{data}' should be of the form " + f"'##dT##h##m##s', '##h##m##s' or ISO8601 duration" + ) + value._data = data + return value + + +class AAZDateFormat(AAZBaseArgFormat): + + def __init__(self): + self.help_string = 'Format: date (yyyy-mm-dd)' + + def __call__(self, ctx, value): + assert isinstance(value, AAZSimpleValue) + data = value._data + if data == AAZUndefined or data is None or value._is_patch: + return value + + assert isinstance(data, str) + import dateutil.parser + import dateutil.tz + + dt_val = None + try: + dt_val = dateutil.parser.parse(data) + except ValueError: + pass + + if not dt_val: + raise AAZInvalidArgValueError( + f"Unable to parse: '{data}'. Expected format: {self.help_string}" + ) + + if any([dt_val.hour, dt_val.minute, dt_val.second, dt_val.microsecond]): + logger.warning('Time info will be ignored in %s.', data) + + if dt_val.tzinfo: + logger.warning('Timezone info will be ignored in %s.', data) + + data = "{:04}-{:02}-{:02}".format(dt_val.year, dt_val.month, dt_val.day) + value._data = data + return value + + +class AAZTimeFormat(AAZBaseArgFormat): + + def __init__(self): + self.help_string = 'Format: time (hh:mm:ss.xxxxxx)' + + def __call__(self, ctx, value): + assert isinstance(value, AAZSimpleValue) + data = value._data + if data == AAZUndefined or data is None or value._is_patch: + return value + + assert isinstance(data, str) + import dateutil.parser + import dateutil.tz + + dt_val = None + try: + dt_val = dateutil.parser.parse(data) + except ValueError: + pass + + if not dt_val: + raise AAZInvalidArgValueError( + f"Unable to parse: '{data}'. Expected format: {self.help_string}" + ) + + if any([dt_val.day, dt_val.month, dt_val.year]): + logger.warning('Date info will be ignored in %s.', data) + + if dt_val.tzinfo: + logger.warning('Timezone info will be ignored in %s.', data) + + data = "{:02}:{:02}:{:02}".format(dt_val.hour, dt_val.minute, dt_val.second) + if dt_val.microsecond: + microseconds = str(dt_val.microsecond).rjust(6, '0').rstrip('0').ljust(3, '0') + data += '.' + microseconds + + value._data = data + return value + + +class AAZDateTimeFormat(AAZBaseArgFormat): + + def __init__(self, protocol="iso"): + self.protocol = protocol + self.help_string = 'Format: date (yyyy-mm-dd) time (hh:mm:ss.xxxxxx) timezone (+/-hh:mm)' + + def __call__(self, ctx, value): + assert isinstance(value, AAZSimpleValue) + data = value._data + if data == AAZUndefined or data is None or value._is_patch: + return value + + assert isinstance(data, str) + import dateutil.parser + import dateutil.tz + from msrest.serialization import Serializer + + dt_val = None + try: + dt_val = dateutil.parser.parse(data) + except ValueError: + pass + + if not dt_val: + raise AAZInvalidArgValueError( + f"Unable to parse: '{data}'. Expected format: {self.help_string}" + ) + + if not dt_val.tzinfo: + dt_val = dt_val.replace(tzinfo=dateutil.tz.tzlocal()) + + if self.protocol == "iso": + data = Serializer.serialize_iso(dt_val) + elif self.protocol == "rfc": + data = Serializer.serialize_rfc(dt_val) + else: + raise NotImplementedError() + + value._data = data + return value + + +class AAZUuidFormat(AAZBaseArgFormat): + + _uuid_pattern = re.compile(r'^[{(]?[0-9a-fA-F]{8}([-]?[0-9a-fA-F]{4}){3}[-]?[0-9a-fA-F]{12}[)}]?$') + + def __init__(self, case=None, braces_removed=True, hyphens_filled=True): + """ + :param case: 'upper' to format data into upper case, 'lower' to format data into lower case + """ + self.case = case + self.braces_removed = braces_removed + self.hyphens_filled = hyphens_filled + + def __call__(self, ctx, value): + assert isinstance(value, AAZSimpleValue) + data = value._data + if data == AAZUndefined or data is None or value._is_patch: + return value + + assert isinstance(data, str) + if not self._uuid_pattern.fullmatch(data): + raise AAZInvalidArgValueError( + f"Invalid format: '{data}' is not a valid GUID or UUID" + ) + + if '-' in data and data.count('-') != 4: + raise AAZInvalidArgValueError( + f"Invalid format: '{data}' is not a valid GUID or UUID" + ) + + if data.startswith('{') or data.endswith('}'): + # remove braces + if not (data.startswith('{') and data.endswith('}')): + raise AAZInvalidArgValueError( + f"Invalid format: '{data}' is not a valid GUID or UUID" + ) + if self.braces_removed: + data = data[1:-1] + + elif data.startswith('(') or data.endswith(')'): + # remove parentheses + if not (data.startswith('(') and data.endswith(')')): + raise AAZInvalidArgValueError( + f"Invalid format: '{data}' is not a valid GUID or UUID" + ) + if self.braces_removed: + data = data[1:-1] + + if '-' not in data and self.hyphens_filled: + # add '-' in data + if data[0] in ('{', '('): + data = f'{data[:9]}-{data[9:13]}-{data[13:17]}-{data[17:21]}-{data[21:]}' + else: + data = f'{data[:8]}-{data[8:12]}-{data[12:16]}-{data[16:20]}-{data[20:]}' + + if self.case == 'upper': + data = data.upper() + elif self.case == 'lower': + data = data.lower() + + value._data = data + return value + + class AAZIntArgFormat(AAZBaseArgFormat): def __init__(self, multiple_of=None, maximum=None, minimum=None): diff --git a/src/azure-cli-core/azure/cli/core/tests/test_aaz_arg_fmt.py b/src/azure-cli-core/azure/cli/core/tests/test_aaz_arg_fmt.py index d4c4024f1dd..697aedbd98c 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_aaz_arg_fmt.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_aaz_arg_fmt.py @@ -57,6 +57,272 @@ def test_str_fmt(self): args = self.format_arg(schema, {"str1": None}) self.assertEqual(args.str1, None) + def test_duration_fmt(self): + from azure.cli.core.aaz import AAZDurationArg, AAZDurationFormat + schema = AAZArgumentsSchema() + schema.duration = AAZDurationArg( + nullable=True + ) + + args = self.format_arg(schema, {"duration": "1s"}) + self.assertEqual(args.duration, "PT1S") + + args = self.format_arg(schema, {"duration": "1m"}) + self.assertEqual(args.duration, "PT1M") + + args = self.format_arg(schema, {"duration": "1h"}) + self.assertEqual(args.duration, "PT1H") + + args = self.format_arg(schema, {"duration": "1d"}) + self.assertEqual(args.duration, "P1D") + + args = self.format_arg(schema, {"duration": "0h1s"}) + self.assertEqual(args.duration, "PT1S") + + args = self.format_arg(schema, {"duration": "1dt1h1m1s"}) + self.assertEqual(args.duration, "P1DT1H1M1S") + + args = self.format_arg(schema, {"duration": "pt1h"}) + self.assertEqual(args.duration, "PT1H") + + args = self.format_arg(schema, {"duration": "p1yt1h"}) + self.assertEqual(args.duration, "P1YT1H") + + args = self.format_arg(schema, {"duration": "p1y"}) + self.assertEqual(args.duration, "P1Y") + + args = self.format_arg(schema, {"duration": "p1m"}) + self.assertEqual(args.duration, "P1M") + + args = self.format_arg(schema, {"duration": "P1Y1M100DT1h1s"}) + self.assertEqual(args.duration, "P1Y1M100DT1H1S") + + args = self.format_arg(schema, {"duration": None}) + self.assertEqual(args.duration, None) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"duration": "1m1d"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"duration": "1x"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"duration": "-1m"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"duration": "m1s"}) + + def test_date_fmt(self): + from azure.cli.core.aaz import AAZDateArg, AAZDateFormat + schema = AAZArgumentsSchema() + schema.date = AAZDateArg( + nullable=True + ) + + args = self.format_arg(schema, {"date": "2010-01-02 01:11:11 +02:00"}) + self.assertEqual(args.date, "2010-01-02") + + args = self.format_arg(schema, {"date": "2010/01/02"}) + self.assertEqual(args.date, "2010-01-02") + + args = self.format_arg(schema, {"date": "October 1 1989"}) + self.assertEqual(args.date, "1989-10-01") + + args = self.format_arg(schema, {"date": "2010/01/02 01:11:11"}) + self.assertEqual(args.date, "2010-01-02") + + args = self.format_arg(schema, {"date": None}) + self.assertEqual(args.date, None) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + args = self.format_arg(schema, {"date": "aaaa"}) + + def test_time_fmt(self): + from azure.cli.core.aaz import AAZTimeArg, AAZTimeFormat + schema = AAZArgumentsSchema() + schema.time = AAZTimeArg( + nullable=True + ) + + args = self.format_arg(schema, {"time": "2010-01-02 01:11:11.12345678 +01:00"}) + self.assertEqual(args.time, "01:11:11.123456") + + args = self.format_arg(schema, {"time": "01:11:11.0001"}) + self.assertEqual(args.time, "01:11:11.0001") + + args = self.format_arg(schema, {"time": "21:11:11.1"}) + self.assertEqual(args.time, "21:11:11.100") + + args = self.format_arg(schema, {"time": "21:01:51.00"}) + self.assertEqual(args.time, "21:01:51") + + args = self.format_arg(schema, {"time": None}) + self.assertEqual(args.time, None) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"time": "21:01:151.00"}) + + def test_datetime_fmt(self): + from azure.cli.core.aaz import AAZDateTimeArg, AAZDateTimeFormat + schema = AAZArgumentsSchema() + schema.datetime = AAZDateTimeArg( + nullable=True + ) + schema.rfc = AAZDateTimeArg( + fmt=AAZDateTimeFormat(protocol='rfc'), + nullable=True + ) + + args = self.format_arg(schema, { + "datetime": "2010-01-02 01:11:11.12345678 -06:30", + "rfc": "2010-01-02 01:11:11.12345678 -06:30", + }) + self.assertEqual(args.datetime, "2010-01-02T07:41:11.123456Z") + self.assertEqual(args.rfc, "Sat, 02 Jan 2010 07:41:11 GMT") + + args = self.format_arg(schema, { + "datetime": "Sat, 02 Jan 2010 07:41:11 GMT", + "rfc": "2010-01-02T07:41:11.123456Z", + }) + self.assertEqual(args.datetime, "2010-01-02T07:41:11.000Z") + self.assertEqual(args.rfc, "Sat, 02 Jan 2010 07:41:11 GMT") + + args = self.format_arg(schema, { + "datetime": "2010/01/02 01:11:11.12345678 +06:30", + "rfc": "2010/01/02 01:11:11.12345678 +06:30", + }) + self.assertEqual(args.datetime, "2010-01-01T18:41:11.123456Z") + self.assertEqual(args.rfc, "Fri, 01 Jan 2010 18:41:11 GMT") + + args = self.format_arg(schema, { + "datetime": "2010-01-02T00:00:00Z", + "rfc": "2010-01-02T00:00:00Z", + }) + self.assertEqual(args.datetime, "2010-01-02T00:00:00.000Z") + self.assertEqual(args.rfc, "Sat, 02 Jan 2010 00:00:00 GMT") + + args = self.format_arg(schema, { + "datetime": "2010-01-02 10:00:00 +00:00", + "rfc": "2010-01-02 10:00:00 +00:00", + }) + self.assertEqual(args.datetime, "2010-01-02T10:00:00.000Z") + self.assertEqual(args.rfc, "Sat, 02 Jan 2010 10:00:00 GMT") + + args = self.format_arg(schema, { + "datetime": None, + "rfc": None, + }) + self.assertEqual(args.datetime, None) + self.assertEqual(args.rfc, None) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + args = self.format_arg(schema, { + "datetime": "-2010-01-a2 10:00:00 +00:00", + }) + + def test_uuid_fmt(self): + from azure.cli.core.aaz import AAZUuidArg, AAZUuidFormat + schema = AAZArgumentsSchema() + schema.uuid = AAZUuidArg( + nullable=True + ) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": ""}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "a8577a7a-4f31-40ab-bb00-3557df24a7e"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "a8577a7a-4f31-40ab-bb003557df24a7ea"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "a8577Z7Z-4f31-40ab-bb00-3557df24a7ea"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "{a8577a7a-4f31-40ab-bb00-3557df24a7ea"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "a8577a7a-4f31-40ab-bb00-3557df24a7ea}"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "(a8577a7a-4f31-40ab-bb00-3557df24a7ea"}) + + with self.assertRaises(azclierror.InvalidArgumentValueError): + self.format_arg(schema, {"uuid": "a8577a7a-4f31-40ab-bb00-3557df24a7ea)"}) + + args = self.format_arg(schema, {"uuid": "a8577a7a-4f31-40ab-bb00-3557df24a7ea"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-bb00-3557df24a7ea") + + args = self.format_arg(schema, {"uuid": "FF0B2DF5-97EB-4E45-B028-8B0BEC803ABE"}) + self.assertEqual(args.uuid, "FF0B2DF5-97EB-4E45-B028-8B0BEC803ABE") + + args = self.format_arg(schema, {"uuid": "a8577A7a-4f31-40Ab-bb00-3557Df24a7Ea"}) + self.assertEqual(args.uuid, "a8577A7a-4f31-40Ab-bb00-3557Df24a7Ea") + + args = self.format_arg(schema, {"uuid": "{a8577a7a-4f31-40ab-bb00-3557df24a7ea}"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-bb00-3557df24a7ea") + + args = self.format_arg(schema, {"uuid": "(a8577a7a-4f31-40ab-bb00-3557df24a7ea)"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-bb00-3557df24a7ea") + + args = self.format_arg(schema, {"uuid": "a8577a7a4f3140abBb003557df24a7eA"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-Bb00-3557df24a7eA") + + args = self.format_arg(schema, {"uuid": "{a8577a7a4f3140abbb003557df24a7ea}"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-bb00-3557df24a7ea") + + args = self.format_arg(schema, {"uuid": "(a8577a7a4f3140abbb003557df24a7ea)"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-bb00-3557df24a7ea") + + schema = AAZArgumentsSchema() + schema.uuid = AAZUuidArg( + fmt=AAZUuidFormat(case='upper'), + nullable=True + ) + + args = self.format_arg(schema, {"uuid": "(a8577a7A4f3140Abbb003557df24a7ea)"}) + self.assertEqual(args.uuid, "A8577A7A-4F31-40AB-BB00-3557DF24A7EA") + + schema = AAZArgumentsSchema() + schema.uuid = AAZUuidArg( + fmt=AAZUuidFormat(case='lower'), + nullable=True + ) + + args = self.format_arg(schema, {"uuid": "(A8577A7A4F3140Abbb003557DF24A7EA)"}) + self.assertEqual(args.uuid, "a8577a7a-4f31-40ab-bb00-3557df24a7ea") + + schema = AAZArgumentsSchema() + schema.uuid = AAZUuidArg( + fmt=AAZUuidFormat(hyphens_filled=False), + nullable=True + ) + + args = self.format_arg(schema, {"uuid": "(A8577A7A4F3140Abbb003557DF24A7EA)"}) + self.assertEqual(args.uuid, "A8577A7A4F3140Abbb003557DF24A7EA") + + schema = AAZArgumentsSchema() + schema.uuid = AAZUuidArg( + fmt=AAZUuidFormat(braces_removed=False), + nullable=True + ) + + args = self.format_arg(schema, {"uuid": "(a8577a7a4f3140abbb003557df24a7ea)"}) + self.assertEqual(args.uuid, "(a8577a7a-4f31-40ab-bb00-3557df24a7ea)") + + schema = AAZArgumentsSchema() + schema.uuid = AAZUuidArg( + fmt=AAZUuidFormat(braces_removed=False, hyphens_filled=False), + nullable=True + ) + + args = self.format_arg(schema, {"uuid": "(A8577A7A4F3140Abbb003557DF24A7EA)"}) + self.assertEqual(args.uuid, "(A8577A7A4F3140Abbb003557DF24A7EA)") + + args = self.format_arg(schema, {"uuid": None}) + self.assertEqual(args.uuid, None) + def test_int_fmt(self): from azure.cli.core.aaz import AAZIntArg, AAZIntArgFormat