From 9460c75ba7a33bfc07c97bc0029f37d17da5fa45 Mon Sep 17 00:00:00 2001 From: Nicola Camillucci Date: Thu, 7 May 2026 15:17:38 +0100 Subject: [PATCH 1/3] Regenerated SDK --- .../_metadata.json | 6 + .../apiview-properties.json | 2 +- .../azure/keyvault/securitydomain/_client.py | 9 +- .../keyvault/securitydomain/_configuration.py | 7 +- .../_internal/http_challenge_cache.py | 1 + .../securitydomain/_operations/__init__.py | 6 +- .../securitydomain/_operations/_operations.py | 58 ++- .../securitydomain/_utils/model_base.py | 421 +++++++++++++----- .../securitydomain/_utils/serialization.py | 43 +- .../keyvault/securitydomain/aio/_client.py | 9 +- .../securitydomain/aio/_configuration.py | 7 +- .../aio/_operations/__init__.py | 6 +- .../aio/_operations/_operations.py | 48 +- .../securitydomain/models/__init__.py | 4 +- .../keyvault/securitydomain/models/_models.py | 44 +- .../azure-keyvault-securitydomain/setup.py | 69 +++ .../tests/jwe.py | 104 +++-- .../tests/wrapping.py | 77 ++-- .../tsp-location.yaml | 6 +- 19 files changed, 632 insertions(+), 295 deletions(-) create mode 100644 sdk/keyvault/azure-keyvault-securitydomain/_metadata.json create mode 100644 sdk/keyvault/azure-keyvault-securitydomain/setup.py diff --git a/sdk/keyvault/azure-keyvault-securitydomain/_metadata.json b/sdk/keyvault/azure-keyvault-securitydomain/_metadata.json new file mode 100644 index 000000000000..0a2924fbf51d --- /dev/null +++ b/sdk/keyvault/azure-keyvault-securitydomain/_metadata.json @@ -0,0 +1,6 @@ +{ + "apiVersion": "2025-07-01", + "apiVersions": { + "KeyVault": "2025-07-01" + } +} \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-securitydomain/apiview-properties.json b/sdk/keyvault/azure-keyvault-securitydomain/apiview-properties.json index 77d0d994a2ac..32474fb35821 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/apiview-properties.json +++ b/sdk/keyvault/azure-keyvault-securitydomain/apiview-properties.json @@ -2,8 +2,8 @@ "CrossLanguagePackageId": "KeyVault", "CrossLanguageDefinitionId": { "azure.keyvault.securitydomain.models.CertificateInfo": "KeyVault.CertificateInfoObject", - "azure.keyvault.securitydomain.models.Error": "Error", "azure.keyvault.securitydomain.models.KeyVaultError": "KeyVaultError", + "azure.keyvault.securitydomain.models.KeyVaultErrorError": "KeyVaultError.error.anonymous", "azure.keyvault.securitydomain.models.SecurityDomain": "KeyVault.SecurityDomainObject", "azure.keyvault.securitydomain.models.SecurityDomainJsonWebKey": "KeyVault.SecurityDomainJsonWebKey", "azure.keyvault.securitydomain.models.SecurityDomainOperationStatus": "KeyVault.SecurityDomainOperationStatus", diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_client.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_client.py index 544d2962647a..2146ec5d1e95 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_client.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_client.py @@ -15,22 +15,23 @@ from azure.core.rest import HttpRequest, HttpResponse from ._configuration import SecurityDomainClientConfiguration -from ._operations import SecurityDomainClientOperationsMixin +from ._operations import _SecurityDomainClientOperationsMixin from ._utils.serialization import Deserializer, Serializer if TYPE_CHECKING: from azure.core.credentials import TokenCredential -class SecurityDomainClient(SecurityDomainClientOperationsMixin): +class SecurityDomainClient(_SecurityDomainClientOperationsMixin): """SecurityDomainClient. :param vault_base_url: Required. :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Default value is "7.5". Note - that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". + Default value is "2025-07-01". Note that overriding this default value may result in + unsupported behavior. :paramtype api_version: str """ diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_configuration.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_configuration.py index 1a036a706db4..138b5d20824e 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_configuration.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_configuration.py @@ -26,13 +26,14 @@ class SecurityDomainClientConfiguration: # pylint: disable=too-many-instance-at :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Default value is "7.5". Note - that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". + Default value is "2025-07-01". Note that overriding this default value may result in + unsupported behavior. :paramtype api_version: str """ def __init__(self, vault_base_url: str, credential: "TokenCredential", **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "7.5") + api_version: str = kwargs.pop("api_version", "2025-07-01") if vault_base_url is None: raise ValueError("Parameter 'vault_base_url' must not be None.") diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_internal/http_challenge_cache.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_internal/http_challenge_cache.py index 6034ff09d5ca..99f32091e24b 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_internal/http_challenge_cache.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_internal/http_challenge_cache.py @@ -63,6 +63,7 @@ def remove_challenge_for_url(url: str) -> None: with _lock: del _cache[key.lower()] + def set_challenge_for_url(url: str, challenge: "HttpChallenge") -> None: """Caches the challenge for the specified URL. diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/__init__.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/__init__.py index c6b747b3914b..1d9a3b7505ad 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/__init__.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/__init__.py @@ -12,14 +12,12 @@ if TYPE_CHECKING: from ._patch import * # pylint: disable=unused-wildcard-import -from ._operations import SecurityDomainClientOperationsMixin # type: ignore +from ._operations import _SecurityDomainClientOperationsMixin # type: ignore # pylint: disable=unused-import from ._patch import __all__ as _patch_all from ._patch import * from ._patch import patch_sdk as _patch_sdk -__all__ = [ - "SecurityDomainClientOperationsMixin", -] +__all__ = [] __all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/_operations.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/_operations.py index 2c0626425a78..7d6d578b7f82 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/_operations.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_operations/_operations.py @@ -8,7 +8,7 @@ from collections.abc import MutableMapping from io import IOBase import json -from typing import Any, Callable, Dict, IO, Iterator, Optional, TypeVar, Union, cast, overload +from typing import Any, Callable, IO, Iterator, Optional, TypeVar, Union, cast, overload from azure.core import PipelineClient from azure.core.exceptions import ( @@ -36,7 +36,7 @@ JSON = MutableMapping[str, Any] T = TypeVar("T") -ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, dict[str, Any]], Any]] _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False @@ -46,7 +46,7 @@ def build_security_domain_get_download_status_request(**kwargs: Any) -> HttpRequ _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "7.5")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -66,7 +66,7 @@ def build_security_domain_download_request(**kwargs: Any) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "7.5")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -87,7 +87,7 @@ def build_security_domain_get_upload_status_request(**kwargs: Any) -> HttpReques _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "7.5")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -107,7 +107,7 @@ def build_security_domain_upload_request(**kwargs: Any) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "7.5")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -128,7 +128,7 @@ def build_security_domain_get_transfer_key_request(**kwargs: Any) -> HttpRequest _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "7.5")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -143,7 +143,9 @@ def build_security_domain_get_transfer_key_request(**kwargs: Any) -> HttpRequest return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) -class SecurityDomainClientOperationsMixin(ClientMixinABC[PipelineClient, SecurityDomainClientConfiguration]): +class _SecurityDomainClientOperationsMixin( + ClientMixinABC[PipelineClient[HttpRequest, HttpResponse], SecurityDomainClientConfiguration] +): @distributed_trace def get_download_status(self, **kwargs: Any) -> _models.SecurityDomainOperationStatus: @@ -179,6 +181,7 @@ def get_download_status(self, **kwargs: Any) -> _models.SecurityDomainOperationS } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -193,11 +196,14 @@ def get_download_status(self, **kwargs: Any) -> _models.SecurityDomainOperationS except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) if _stream: - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: deserialized = _deserialize(_models.SecurityDomainOperationStatus, response.json()) @@ -244,6 +250,7 @@ def _download_initial( } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = True pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -257,7 +264,10 @@ def _download_initial( except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) response_headers = {} @@ -266,7 +276,7 @@ def _download_initial( ) response_headers["Retry-After"] = self._deserialize("int", response.headers.get("Retry-After")) - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -383,6 +393,7 @@ def get_upload_status(self, **kwargs: Any) -> _models.SecurityDomainOperationSta } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -397,11 +408,14 @@ def get_upload_status(self, **kwargs: Any) -> _models.SecurityDomainOperationSta except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) if _stream: - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: deserialized = _deserialize(_models.SecurityDomainOperationStatus, response.json()) @@ -448,6 +462,7 @@ def _upload_initial( } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = True pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -461,7 +476,10 @@ def _upload_initial( except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) response_headers = {} @@ -471,7 +489,7 @@ def _upload_initial( ) response_headers["Retry-After"] = self._deserialize("int", response.headers.get("Retry-After")) - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -598,6 +616,7 @@ def get_transfer_key(self, **kwargs: Any) -> _models.TransferKey: } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -612,11 +631,14 @@ def get_transfer_key(self, **kwargs: Any) -> _models.TransferKey: except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) if _stream: - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: deserialized = _deserialize(_models.TransferKey, response.json()) diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/model_base.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/model_base.py index 49d5c7259389..db24930fdca9 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/model_base.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/model_base.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-lines +# pylint: disable=line-too-long,useless-suppression,too-many-lines # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. @@ -29,6 +29,7 @@ from azure.core import CaseInsensitiveEnumMeta from azure.core.pipeline import PipelineResponse from azure.core.serialization import _Null +from azure.core.rest import HttpResponse _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,7 @@ TZ_UTC = timezone.utc _T = typing.TypeVar("_T") +_NONE_TYPE = type(None) def _timedelta_as_isostr(td: timedelta) -> str: @@ -170,6 +172,21 @@ def default(self, o): # pylint: disable=too-many-return-statements r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT" ) +_ARRAY_ENCODE_MAPPING = { + "pipeDelimited": "|", + "spaceDelimited": " ", + "commaDelimited": ",", + "newlineDelimited": "\n", +} + + +def _deserialize_array_encoded(delimit: str, attr): + if isinstance(attr, str): + if attr == "": + return [] + return attr.split(delimit) + return attr + def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: """Deserialize ISO-8601 formatted string into Datetime object. @@ -201,7 +218,7 @@ def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: test_utc = date_obj.utctimetuple() if test_utc.tm_year > 9999 or test_utc.tm_year < 1: raise OverflowError("Hit max or min date") - return date_obj + return date_obj # type: ignore[no-any-return] def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime: @@ -255,7 +272,7 @@ def _deserialize_time(attr: typing.Union[str, time]) -> time: """ if isinstance(attr, time): return attr - return isodate.parse_time(attr) + return isodate.parse_time(attr) # type: ignore[no-any-return] def _deserialize_bytes(attr): @@ -314,6 +331,8 @@ def _deserialize_int_as_str(attr): def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None): if annotation is int and rf and rf._format == "str": return _deserialize_int_as_str + if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING: + return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format]) if rf and rf._format: return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore @@ -345,16 +364,46 @@ def _get_model(module_name: str, model_name: str): class _MyMutableMapping(MutableMapping[str, typing.Any]): - def __init__(self, data: typing.Dict[str, typing.Any]) -> None: + def __init__(self, data: dict[str, typing.Any]) -> None: self._data = data def __contains__(self, key: typing.Any) -> bool: return key in self._data def __getitem__(self, key: str) -> typing.Any: + # If this key has been deserialized (for mutable types), we need to handle serialization + if hasattr(self, "_attr_to_rest_field"): + cache_attr = f"_deserialized_{key}" + if hasattr(self, cache_attr): + rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key) + if rf: + value = self._data.get(key) + if isinstance(value, (dict, list, set)): + # For mutable types, serialize and return + # But also update _data with serialized form and clear flag + # so mutations via this returned value affect _data + serialized = _serialize(value, rf._format) + # If serialized form is same type (no transformation needed), + # return _data directly so mutations work + if isinstance(serialized, type(value)) and serialized == value: + return self._data.get(key) + # Otherwise return serialized copy and clear flag + try: + object.__delattr__(self, cache_attr) + except AttributeError: + pass + # Store serialized form back + self._data[key] = serialized + return serialized return self._data.__getitem__(key) def __setitem__(self, key: str, value: typing.Any) -> None: + # Clear any cached deserialized value when setting through dictionary access + cache_attr = f"_deserialized_{key}" + try: + object.__delattr__(self, cache_attr) + except AttributeError: + pass self._data.__setitem__(key, value) def __delitem__(self, key: str) -> None: @@ -425,7 +474,7 @@ def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any: return self._data.pop(key) return self._data.pop(key, default) - def popitem(self) -> typing.Tuple[str, typing.Any]: + def popitem(self) -> tuple[str, typing.Any]: """ Removes and returns some (key, value) pair :returns: The (key, value) pair. @@ -466,6 +515,8 @@ def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any: return self._data.setdefault(key, default) def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, _MyMutableMapping): + return self._data == other._data try: other_model = self.__class__(other) except Exception: @@ -482,6 +533,8 @@ def _is_model(obj: typing.Any) -> bool: def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements if isinstance(o, list): + if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o): + return _ARRAY_ENCODE_MAPPING[format].join(o) return [_serialize(x, format) for x in o] if isinstance(o, dict): return {k: _serialize(v, format) for k, v in o.items()} @@ -513,9 +566,7 @@ def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-m return o -def _get_rest_field( - attr_to_rest_field: typing.Dict[str, "_RestField"], rest_name: str -) -> typing.Optional["_RestField"]: +def _get_rest_field(attr_to_rest_field: dict[str, "_RestField"], rest_name: str) -> typing.Optional["_RestField"]: try: return next(rf for rf in attr_to_rest_field.values() if rf._rest_name == rest_name) except StopIteration: @@ -538,7 +589,7 @@ class Model(_MyMutableMapping): _is_model = True # label whether current class's _attr_to_rest_field has been calculated # could not see _attr_to_rest_field directly because subclass inherits it from parent class - _calculated: typing.Set[str] = set() + _calculated: set[str] = set() def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: class_name = self.__class__.__name__ @@ -549,54 +600,9 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: for rest_field in self._attr_to_rest_field.values() if rest_field._default is not _UNSET } - if args: # pylint: disable=too-many-nested-blocks + if args: if isinstance(args[0], ET.Element): - existed_attr_keys = [] - model_meta = getattr(self, "_xml", {}) - - for rf in self._attr_to_rest_field.values(): - prop_meta = getattr(rf, "_xml", {}) - xml_name = prop_meta.get("name", rf._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - # attribute - if prop_meta.get("attribute", False) and args[0].get(xml_name) is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].get(xml_name)) - continue - - # unwrapped element is array - if prop_meta.get("unwrapped", False): - # unwrapped array could either use prop items meta/prop meta - if prop_meta.get("itemsName"): - xml_name = prop_meta.get("itemsName") - xml_ns = prop_meta.get("itemNs") - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - items = args[0].findall(xml_name) # pyright: ignore - if len(items) > 0: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, items) - continue - - # text element is primitive type - if prop_meta.get("text", False): - if args[0].text is not None: - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].text) - continue - - # wrapped element could be normal property or array, it should only have one element - item = args[0].find(xml_name) - if item is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, item) - - # rest thing is additional properties - for e in args[0]: - if e.tag not in existed_attr_keys: - dict_to_pass[e.tag] = _convert_element(e) + dict_to_pass.update(self._init_from_xml(args[0])) else: dict_to_pass.update( {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()} @@ -615,6 +621,69 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: ) super().__init__(dict_to_pass) + def _init_from_xml(self, element: ET.Element) -> dict[str, typing.Any]: + """Deserialize an XML element into a dict mapping rest field names to values. + + :param ET.Element element: The XML element to deserialize from. + :returns: A dictionary of rest_name to deserialized value pairs. + :rtype: dict + """ + result: dict[str, typing.Any] = {} + model_meta = getattr(self, "_xml", {}) + existed_attr_keys: list[str] = [] + + for rf in self._attr_to_rest_field.values(): + prop_meta = getattr(rf, "_xml", {}) + xml_name = prop_meta.get("name", rf._rest_name) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + + # attribute + if prop_meta.get("attribute", False) and element.get(xml_name) is not None: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name)) + continue + + # unwrapped element is array + if prop_meta.get("unwrapped", False): + # unwrapped array could either use prop items meta/prop meta + _items_name = prop_meta.get("itemsName") + if _items_name: + xml_name = _items_name + _items_ns = prop_meta.get("itemsNs") + if _items_ns is not None: + xml_ns = _items_ns + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + items = element.findall(xml_name) # pyright: ignore + if len(items) > 0: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, items) + elif not rf._is_optional: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = [] + continue + + # text element is primitive type + if prop_meta.get("text", False): + if element.text is not None: + result[rf._rest_name] = _deserialize(rf._type, element.text) + continue + + # wrapped element could be normal property or array, it should only have one element + item = element.find(xml_name) + if item is not None: + existed_attr_keys.append(xml_name) + result[rf._rest_name] = _deserialize(rf._type, item) + + # rest thing is additional properties + for e in element: + if e.tag not in existed_attr_keys: + result[e.tag] = _convert_element(e) + + return result + def copy(self) -> "Model": return Model(self.__dict__) @@ -623,7 +692,7 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping', # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object' mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order - attr_to_rest_field: typing.Dict[str, _RestField] = { # map attribute name to rest_field property + attr_to_rest_field: dict[str, _RestField] = { # map attribute name to rest_field property k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type") } annotations = { @@ -638,7 +707,7 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None)) if not rf._rest_name_input: rf._rest_name_input = attr - cls._attr_to_rest_field: typing.Dict[str, _RestField] = dict(attr_to_rest_field.items()) + cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items()) cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") return super().__new__(cls) @@ -667,7 +736,7 @@ def _deserialize(cls, data, exist_discriminators): model_meta = getattr(cls, "_xml", {}) prop_meta = getattr(discriminator, "_xml", {}) xml_name = prop_meta.get("name", discriminator._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) + xml_ns = _resolve_xml_ns(prop_meta, model_meta) if xml_ns: xml_name = "{" + xml_ns + "}" + xml_name @@ -680,7 +749,7 @@ def _deserialize(cls, data, exist_discriminators): mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore # pylint: disable=no-member return mapped_cls._deserialize(data, exist_discriminators) - def as_dict(self, *, exclude_readonly: bool = False) -> typing.Dict[str, typing.Any]: + def as_dict(self, *, exclude_readonly: bool = False) -> dict[str, typing.Any]: """Return a dict that can be turned into json using json.dump. :keyword bool exclude_readonly: Whether to remove the readonly properties. @@ -740,7 +809,7 @@ def _deserialize_with_union(deserializers, obj): def _deserialize_dict( value_deserializer: typing.Optional[typing.Callable], module: typing.Optional[str], - obj: typing.Dict[typing.Any, typing.Any], + obj: dict[typing.Any, typing.Any], ): if obj is None: return obj @@ -750,7 +819,7 @@ def _deserialize_dict( def _deserialize_multiple_sequence( - entry_deserializers: typing.List[typing.Optional[typing.Callable]], + entry_deserializers: list[typing.Optional[typing.Callable]], module: typing.Optional[str], obj, ): @@ -759,6 +828,14 @@ def _deserialize_multiple_sequence( return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers)) +def _is_array_encoded_deserializer(deserializer: functools.partial) -> bool: + return ( + isinstance(deserializer, functools.partial) + and isinstance(deserializer.args[0], functools.partial) + and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable + ) + + def _deserialize_sequence( deserializer: typing.Optional[typing.Callable], module: typing.Optional[str], @@ -768,17 +845,30 @@ def _deserialize_sequence( return obj if isinstance(obj, ET.Element): obj = list(obj) + + # encoded string may be deserialized to sequence + if isinstance(obj, str) and isinstance(deserializer, functools.partial): + # for list[str] + if _is_array_encoded_deserializer(deserializer): + return deserializer(obj) + + # for list[Union[...]] + if isinstance(deserializer.args[0], list): + for sub_deserializer in deserializer.args[0]: + if _is_array_encoded_deserializer(sub_deserializer): + return sub_deserializer(obj) + return type(obj)(_deserialize(deserializer, entry, module) for entry in obj) -def _sorted_annotations(types: typing.List[typing.Any]) -> typing.List[typing.Any]: +def _sorted_annotations(types: list[typing.Any]) -> list[typing.Any]: return sorted( types, key=lambda x: hasattr(x, "__name__") and x.__name__.lower() in ("str", "float", "int", "bool"), ) -def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-branches +def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-statements, too-many-branches annotation: typing.Any, module: typing.Optional[str], rf: typing.Optional["_RestField"] = None, @@ -818,16 +908,18 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur # is it optional? try: - if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore + if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore + if rf: + rf._is_optional = True if len(annotation.__args__) <= 2: # pyright: ignore if_obj_deserializer = _get_deserialize_callable_from_annotation( - next(a for a in annotation.__args__ if a != type(None)), module, rf # pyright: ignore + next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore ) return functools.partial(_deserialize_with_optional, if_obj_deserializer) # the type is Optional[Union[...]], we need to remove the None type from the Union annotation_copy = copy.copy(annotation) - annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a != type(None)] # pyright: ignore + annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a is not _NONE_TYPE] # pyright: ignore return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) except AttributeError: pass @@ -843,7 +935,10 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur return functools.partial(_deserialize_with_union, deserializers) try: - if annotation._name == "Dict": # pyright: ignore + annotation_name = ( + annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore + ) + if annotation_name.lower() == "dict": value_deserializer = _get_deserialize_callable_from_annotation( annotation.__args__[1], module, rf # pyright: ignore ) @@ -856,7 +951,10 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur except (AttributeError, IndexError): pass try: - if annotation._name in ["List", "Set", "Tuple", "Sequence"]: # pyright: ignore + annotation_name = ( + annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore + ) + if annotation_name.lower() in ["list", "set", "tuple", "sequence"]: if len(annotation.__args__) > 1: # pyright: ignore entry_deserializers = [ _get_deserialize_callable_from_annotation(dt, module, rf) @@ -905,16 +1003,20 @@ def _deserialize_with_callable( return float(value.text) if value.text else None if deserializer is bool: return value.text == "true" if value.text else None + if deserializer and deserializer in _DESERIALIZE_MAPPING.values(): + return deserializer(value.text) if value.text else None + if deserializer and deserializer in _DESERIALIZE_MAPPING_WITHFORMAT.values(): + return deserializer(value.text) if value.text else None if deserializer is None: return value if deserializer in [int, float, bool]: return deserializer(value) if isinstance(deserializer, CaseInsensitiveEnumMeta): try: - return deserializer(value) + return deserializer(value.text if isinstance(value, ET.Element) else value) except ValueError: # for unknown value, return raw value - return value + return value.text if isinstance(value, ET.Element) else value if isinstance(deserializer, type) and issubclass(deserializer, Model): return deserializer._deserialize(value, []) return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value) @@ -940,14 +1042,14 @@ def _deserialize( def _failsafe_deserialize( deserializer: typing.Any, - value: typing.Any, + response: HttpResponse, module: typing.Optional[str] = None, rf: typing.Optional["_RestField"] = None, format: typing.Optional[str] = None, ) -> typing.Any: try: - return _deserialize(deserializer, value, module, rf, format) - except DeserializationError: + return _deserialize(deserializer, response.json(), module, rf, format) + except Exception: # pylint: disable=broad-except _LOGGER.warning( "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True ) @@ -956,17 +1058,18 @@ def _failsafe_deserialize( def _failsafe_deserialize_xml( deserializer: typing.Any, - value: typing.Any, + response: HttpResponse, ) -> typing.Any: try: - return _deserialize_xml(deserializer, value) - except DeserializationError: + return _deserialize_xml(deserializer, response.text()) + except Exception: # pylint: disable=broad-except _LOGGER.warning( "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True ) return None +# pylint: disable=too-many-instance-attributes class _RestField: def __init__( self, @@ -974,11 +1077,11 @@ def __init__( name: typing.Optional[str] = None, type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin is_discriminator: bool = False, - visibility: typing.Optional[typing.List[str]] = None, + visibility: typing.Optional[list[str]] = None, default: typing.Any = _UNSET, format: typing.Optional[str] = None, is_multipart_file_input: bool = False, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, + xml: typing.Optional[dict[str, typing.Any]] = None, ): self._type = type self._rest_name_input = name @@ -986,6 +1089,7 @@ def __init__( self._is_discriminator = is_discriminator self._visibility = visibility self._is_model = False + self._is_optional = False self._default = default self._format = format self._is_multipart_file_input = is_multipart_file_input @@ -993,7 +1097,11 @@ def __init__( @property def _class_type(self) -> typing.Any: - return getattr(self._type, "args", [None])[0] + result = getattr(self._type, "args", [None])[0] + # type may be wrapped by nested functools.partial so we need to check for that + if isinstance(result, functools.partial): + return getattr(result, "args", [None])[0] + return result @property def _rest_name(self) -> str: @@ -1004,14 +1112,37 @@ def _rest_name(self) -> str: def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin # by this point, type and rest_name will have a value bc we default # them in __new__ of the Model class - item = obj.get(self._rest_name) + # Use _data.get() directly to avoid triggering __getitem__ which clears the cache + item = obj._data.get(self._rest_name) if item is None: return item if self._is_model: return item - return _deserialize(self._type, _serialize(item, self._format), rf=self) + + # For mutable types, we want mutations to directly affect _data + # Check if we've already deserialized this value + cache_attr = f"_deserialized_{self._rest_name}" + if hasattr(obj, cache_attr): + # Return the value from _data directly (it's been deserialized in place) + return obj._data.get(self._rest_name) + + deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self) + + # For mutable types, store the deserialized value back in _data + # so mutations directly affect _data + if isinstance(deserialized, (dict, list, set)): + obj._data[self._rest_name] = deserialized + object.__setattr__(obj, cache_attr, True) # Mark as deserialized + return deserialized + + return deserialized def __set__(self, obj: Model, value) -> None: + # Clear the cached deserialized object when setting a new value + cache_attr = f"_deserialized_{self._rest_name}" + if hasattr(obj, cache_attr): + object.__delattr__(obj, cache_attr) + if value is None: # we want to wipe out entries if users set attr to None try: @@ -1036,11 +1167,11 @@ def rest_field( *, name: typing.Optional[str] = None, type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[typing.List[str]] = None, + visibility: typing.Optional[list[str]] = None, default: typing.Any = _UNSET, format: typing.Optional[str] = None, is_multipart_file_input: bool = False, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, + xml: typing.Optional[dict[str, typing.Any]] = None, ) -> typing.Any: return _RestField( name=name, @@ -1057,8 +1188,8 @@ def rest_discriminator( *, name: typing.Optional[str] = None, type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[typing.List[str]] = None, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, + visibility: typing.Optional[list[str]] = None, + xml: typing.Optional[dict[str, typing.Any]] = None, ) -> typing.Any: return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml) @@ -1074,21 +1205,77 @@ def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore +def _get_xml_ns(meta: dict[str, typing.Any]) -> typing.Optional[str]: + """Return the XML namespace from a metadata dict, checking both 'ns' (old-style) and 'namespace' (DPG) keys. + + :param dict meta: The metadata dictionary to extract namespace from. + :returns: The namespace string if 'ns' or 'namespace' key is present, None otherwise. + :rtype: str or None + """ + ns = meta.get("ns") + if ns is None: + ns = meta.get("namespace") + return ns + + +def _resolve_xml_ns( + prop_meta: dict[str, typing.Any], model_meta: typing.Optional[dict[str, typing.Any]] = None +) -> typing.Optional[str]: + """Resolve XML namespace for a property, falling back to model namespace when appropriate. + + Checks the property metadata first; if no namespace is found and the model does not declare + an explicit prefix, falls back to the model-level namespace. + + :param dict prop_meta: The property metadata dictionary. + :param dict model_meta: The model metadata dictionary, used as fallback. + :returns: The resolved namespace string, or None. + :rtype: str or None + """ + ns = _get_xml_ns(prop_meta) + if ns is None and model_meta is not None and not model_meta.get("prefix"): + ns = _get_xml_ns(model_meta) + return ns + + +def _set_xml_attribute(element: ET.Element, name: str, value: typing.Any, prop_meta: dict[str, typing.Any]) -> None: + """Set an XML attribute on an element, handling namespace prefix registration. + + :param ET.Element element: The element to set the attribute on. + :param str name: The default attribute name (wire name). + :param any value: The attribute value. + :param dict prop_meta: The property metadata dictionary. + """ + xml_name = prop_meta.get("name", name) + _attr_ns = _get_xml_ns(prop_meta) + if _attr_ns: + _attr_prefix = prop_meta.get("prefix") + if _attr_prefix: + _safe_register_namespace(_attr_prefix, _attr_ns) + xml_name = "{" + _attr_ns + "}" + xml_name + element.set(xml_name, _get_primitive_type_value(value)) + + def _get_element( o: typing.Any, exclude_readonly: bool = False, - parent_meta: typing.Optional[typing.Dict[str, typing.Any]] = None, + parent_meta: typing.Optional[dict[str, typing.Any]] = None, wrapped_element: typing.Optional[ET.Element] = None, -) -> typing.Union[ET.Element, typing.List[ET.Element]]: +) -> typing.Union[ET.Element, list[ET.Element]]: if _is_model(o): model_meta = getattr(o, "_xml", {}) # if prop is a model, then use the prop element directly, else generate a wrapper of model if wrapped_element is None: + # When serializing as an array item (parent_meta is set), check if the parent has an + # explicit itemsName. This ensures correct element names for unwrapped arrays (where + # the element tag is the property/items name, not the model type name). + _items_name = parent_meta.get("itemsName") if parent_meta is not None else None + element_name = _items_name if _items_name else (model_meta.get("name") or o.__class__.__name__) + _model_ns = _get_xml_ns(model_meta) wrapped_element = _create_xml_element( - model_meta.get("name", o.__class__.__name__), + element_name, model_meta.get("prefix"), - model_meta.get("ns"), + _model_ns, ) readonly_props = [] @@ -1110,7 +1297,9 @@ def _get_element( # additional properties will not have rest field, use the wire name as xml name prop_meta = {"name": k} - # if no ns for prop, use model's + # Propagate model namespace to properties only for old-style "ns"-keyed models. + # DPG-generated models use the "namespace" key and explicitly declare namespace on + # each property that needs it, so propagation is intentionally skipped for them. if prop_meta.get("ns") is None and model_meta.get("ns"): prop_meta["ns"] = model_meta.get("ns") prop_meta["prefix"] = model_meta.get("prefix") @@ -1122,12 +1311,7 @@ def _get_element( # text could only set on primitive type wrapped_element.text = _get_primitive_type_value(v) elif prop_meta.get("attribute", False): - xml_name = prop_meta.get("name", k) - if prop_meta.get("ns"): - ET.register_namespace(prop_meta.get("prefix"), prop_meta.get("ns")) # pyright: ignore - xml_name = "{" + prop_meta.get("ns") + "}" + xml_name # pyright: ignore - # attribute should be primitive type - wrapped_element.set(xml_name, _get_primitive_type_value(v)) + _set_xml_attribute(wrapped_element, k, v, prop_meta) else: # other wrapped prop element wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) @@ -1136,6 +1320,7 @@ def _get_element( return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore if isinstance(o, dict): result = [] + _dict_ns = _get_xml_ns(parent_meta) if parent_meta else None for k, v in o.items(): result.append( _get_wrapped_element( @@ -1143,7 +1328,7 @@ def _get_element( exclude_readonly, { "name": k, - "ns": parent_meta.get("ns") if parent_meta else None, + "ns": _dict_ns, "prefix": parent_meta.get("prefix") if parent_meta else None, }, ) @@ -1152,13 +1337,16 @@ def _get_element( # primitive case need to create element based on parent_meta if parent_meta: + _items_ns = parent_meta.get("itemsNs") + if _items_ns is None: + _items_ns = _get_xml_ns(parent_meta) return _get_wrapped_element( o, exclude_readonly, { "name": parent_meta.get("itemsName", parent_meta.get("name")), "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")), - "ns": parent_meta.get("itemsNs", parent_meta.get("ns")), + "ns": _items_ns, }, ) @@ -1168,10 +1356,11 @@ def _get_element( def _get_wrapped_element( v: typing.Any, exclude_readonly: bool, - meta: typing.Optional[typing.Dict[str, typing.Any]], + meta: typing.Optional[dict[str, typing.Any]], ) -> ET.Element: + _meta_ns = _get_xml_ns(meta) if meta else None wrapped_element = _create_xml_element( - meta.get("name") if meta else None, meta.get("prefix") if meta else None, meta.get("ns") if meta else None + meta.get("name") if meta else None, meta.get("prefix") if meta else None, _meta_ns ) if isinstance(v, (dict, list)): wrapped_element.extend(_get_element(v, exclude_readonly, meta)) @@ -1179,7 +1368,7 @@ def _get_wrapped_element( _get_element(v, exclude_readonly, meta, wrapped_element) else: wrapped_element.text = _get_primitive_type_value(v) - return wrapped_element + return wrapped_element # type: ignore[no-any-return] def _get_primitive_type_value(v) -> str: @@ -1192,9 +1381,29 @@ def _get_primitive_type_value(v) -> str: return str(v) -def _create_xml_element(tag, prefix=None, ns=None): - if prefix and ns: +def _safe_register_namespace(prefix: str, ns: str) -> None: + """Register an XML namespace prefix, handling reserved prefix patterns. + + Some prefixes (e.g. 'ns2') match Python's reserved 'ns\\d+' pattern used for + auto-generated prefixes, causing register_namespace to raise ValueError. + Falls back to directly registering in the internal namespace map. + + :param str prefix: The namespace prefix to register. + :param str ns: The namespace URI. + """ + try: ET.register_namespace(prefix, ns) + except ValueError: + _ns_map = getattr(ET, "_namespace_map", None) + if _ns_map is not None: + _ns_map[ns] = prefix + + +def _create_xml_element( + tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None +) -> ET.Element: + if prefix and ns: + _safe_register_namespace(prefix, ns) if ns: return ET.Element("{" + ns + "}" + tag) return ET.Element(tag) @@ -1211,7 +1420,7 @@ def _deserialize_xml( def _convert_element(e: ET.Element): # dict case if len(e.attrib) > 0 or len({child.tag for child in e}) > 1: - dict_result: typing.Dict[str, typing.Any] = {} + dict_result: dict[str, typing.Any] = {} for child in e: if dict_result.get(child.tag) is not None: if isinstance(dict_result[child.tag], list): @@ -1224,7 +1433,7 @@ def _convert_element(e: ET.Element): return dict_result # array case if len(e) > 0: - array_result: typing.List[typing.Any] = [] + array_result: list[typing.Any] = [] for child in e: array_result.append(_convert_element(child)) return array_result diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/serialization.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/serialization.py index eb86ea23c965..81ec1de5922b 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/serialization.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_utils/serialization.py @@ -21,7 +21,6 @@ import sys import codecs from typing import ( - Dict, Any, cast, Optional, @@ -31,7 +30,6 @@ Mapping, Callable, MutableMapping, - List, ) try: @@ -229,12 +227,12 @@ class Model: serialization and deserialization. """ - _subtype_map: Dict[str, Dict[str, Any]] = {} - _attribute_map: Dict[str, Dict[str, Any]] = {} - _validation: Dict[str, Dict[str, Any]] = {} + _subtype_map: dict[str, dict[str, Any]] = {} + _attribute_map: dict[str, dict[str, Any]] = {} + _validation: dict[str, dict[str, Any]] = {} def __init__(self, **kwargs: Any) -> None: - self.additional_properties: Optional[Dict[str, Any]] = {} + self.additional_properties: Optional[dict[str, Any]] = {} for k in kwargs: # pylint: disable=consider-using-dict-items if k not in self._attribute_map: _LOGGER.warning("%s is not a known attribute of class %s and will be ignored", k, self.__class__) @@ -311,7 +309,7 @@ def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: def as_dict( self, keep_readonly: bool = True, - key_transformer: Callable[[str, Dict[str, Any], Any], Any] = attribute_transformer, + key_transformer: Callable[[str, dict[str, Any], Any], Any] = attribute_transformer, **kwargs: Any ) -> JSON: """Return a dict that can be serialized using json.dump. @@ -380,7 +378,7 @@ def deserialize(cls, data: Any, content_type: Optional[str] = None) -> Self: def from_dict( cls, data: Any, - key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, + key_extractors: Optional[Callable[[str, dict[str, Any], Any], Any]] = None, content_type: Optional[str] = None, ) -> Self: """Parse a dict using given key extractor return a model. @@ -414,7 +412,7 @@ def _flatten_subtype(cls, key, objects): return {} result = dict(cls._subtype_map[key]) for valuetype in cls._subtype_map[key].values(): - result.update(objects[valuetype]._flatten_subtype(key, objects)) # pylint: disable=protected-access + result |= objects[valuetype]._flatten_subtype(key, objects) # pylint: disable=protected-access return result @classmethod @@ -528,7 +526,7 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: "[]": self.serialize_iter, "{}": self.serialize_dict, } - self.dependencies: Dict[str, type] = dict(classes) if classes else {} + self.dependencies: dict[str, type] = dict(classes) if classes else {} self.key_transformer = full_restapi_key_transformer self.client_side_validation = True @@ -579,7 +577,7 @@ def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, to if attr_name == "additional_properties" and attr_desc["key"] == "": if target_obj.additional_properties is not None: - serialized.update(target_obj.additional_properties) + serialized |= target_obj.additional_properties continue try: @@ -789,7 +787,7 @@ def serialize_data(self, data, data_type, **kwargs): # If dependencies is empty, try with current data class # It has to be a subclass of Enum anyway - enum_type = self.dependencies.get(data_type, data.__class__) + enum_type = self.dependencies.get(data_type, cast(type, data.__class__)) if issubclass(enum_type, Enum): return Serializer.serialize_enum(data, enum_obj=enum_type) @@ -823,13 +821,20 @@ def serialize_basic(cls, data, data_type, **kwargs): :param str data_type: Type of object in the iterable. :rtype: str, int, float, bool :return: serialized object + :raises TypeError: raise if data_type is not one of str, int, float, bool. """ custom_serializer = cls._get_custom_serializers(data_type, **kwargs) if custom_serializer: return custom_serializer(data) if data_type == "str": return cls.serialize_unicode(data) - return eval(data_type)(data) # nosec # pylint: disable=eval-used + if data_type == "int": + return int(data) + if data_type == "float": + return float(data) + if data_type == "bool": + return bool(data) + raise TypeError("Unknown basic data type: {}".format(data_type)) @classmethod def serialize_unicode(cls, data): @@ -1184,7 +1189,7 @@ def rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argumen while "." in key: # Need the cast, as for some reasons "split" is typed as list[str | Any] - dict_keys = cast(List[str], _FLATTEN.split(key)) + dict_keys = cast(list[str], _FLATTEN.split(key)) if len(dict_keys) == 1: key = _decode_attribute_map_key(dict_keys[0]) break @@ -1386,7 +1391,7 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: "duration": (isodate.Duration, datetime.timedelta), "iso-8601": (datetime.datetime), } - self.dependencies: Dict[str, type] = dict(classes) if classes else {} + self.dependencies: dict[str, type] = dict(classes) if classes else {} self.key_extractors = [rest_key_extractor, xml_key_extractor] # Additional properties only works if the "rest_key_extractor" is used to # extract the keys. Making it to work whatever the key extractor is too much @@ -1759,7 +1764,7 @@ def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return :param str data_type: deserialization data type. :return: Deserialized basic type. :rtype: str, int, float or bool - :raises TypeError: if string format is not valid. + :raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool. """ # If we're here, data is supposed to be a basic type. # If it's still an XML node, take the text @@ -1785,7 +1790,11 @@ def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return if data_type == "str": return self.deserialize_unicode(attr) - return eval(data_type)(attr) # nosec # pylint: disable=eval-used + if data_type == "int": + return int(attr) + if data_type == "float": + return float(attr) + raise TypeError("Unknown basic data type: {}".format(data_type)) @staticmethod def deserialize_unicode(data): diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_client.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_client.py index 269b75785754..baaa061217d6 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_client.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_client.py @@ -16,21 +16,22 @@ from .._utils.serialization import Deserializer, Serializer from ._configuration import SecurityDomainClientConfiguration -from ._operations import SecurityDomainClientOperationsMixin +from ._operations import _SecurityDomainClientOperationsMixin if TYPE_CHECKING: from azure.core.credentials_async import AsyncTokenCredential -class SecurityDomainClient(SecurityDomainClientOperationsMixin): +class SecurityDomainClient(_SecurityDomainClientOperationsMixin): """SecurityDomainClient. :param vault_base_url: Required. :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Default value is "7.5". Note - that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". + Default value is "2025-07-01". Note that overriding this default value may result in + unsupported behavior. :paramtype api_version: str """ diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_configuration.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_configuration.py index bd16d340efbe..8a93ab314f01 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_configuration.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_configuration.py @@ -26,13 +26,14 @@ class SecurityDomainClientConfiguration: # pylint: disable=too-many-instance-at :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Default value is "7.5". Note - that overriding this default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". + Default value is "2025-07-01". Note that overriding this default value may result in + unsupported behavior. :paramtype api_version: str """ def __init__(self, vault_base_url: str, credential: "AsyncTokenCredential", **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "7.5") + api_version: str = kwargs.pop("api_version", "2025-07-01") if vault_base_url is None: raise ValueError("Parameter 'vault_base_url' must not be None.") diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/__init__.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/__init__.py index c6b747b3914b..1d9a3b7505ad 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/__init__.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/__init__.py @@ -12,14 +12,12 @@ if TYPE_CHECKING: from ._patch import * # pylint: disable=unused-wildcard-import -from ._operations import SecurityDomainClientOperationsMixin # type: ignore +from ._operations import _SecurityDomainClientOperationsMixin # type: ignore # pylint: disable=unused-import from ._patch import __all__ as _patch_all from ._patch import * from ._patch import patch_sdk as _patch_sdk -__all__ = [ - "SecurityDomainClientOperationsMixin", -] +__all__ = [] __all__.extend([p for p in _patch_all if p not in __all__]) # pyright: ignore _patch_sdk() diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/_operations.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/_operations.py index b461fa171b9f..0f99e1c64c66 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/_operations.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_operations/_operations.py @@ -9,7 +9,7 @@ from collections.abc import MutableMapping from io import IOBase import json -from typing import Any, AsyncIterator, Callable, Dict, IO, Optional, TypeVar, Union, cast, overload +from typing import Any, AsyncIterator, Callable, IO, Optional, TypeVar, Union, cast, overload from azure.core import AsyncPipelineClient from azure.core.exceptions import ( @@ -43,10 +43,12 @@ JSON = MutableMapping[str, Any] T = TypeVar("T") -ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, dict[str, Any]], Any]] -class SecurityDomainClientOperationsMixin(ClientMixinABC[AsyncPipelineClient, SecurityDomainClientConfiguration]): +class _SecurityDomainClientOperationsMixin( + ClientMixinABC[AsyncPipelineClient[HttpRequest, AsyncHttpResponse], SecurityDomainClientConfiguration] +): @distributed_trace_async async def get_download_status(self, **kwargs: Any) -> _models.SecurityDomainOperationStatus: @@ -82,6 +84,7 @@ async def get_download_status(self, **kwargs: Any) -> _models.SecurityDomainOper } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -96,11 +99,14 @@ async def get_download_status(self, **kwargs: Any) -> _models.SecurityDomainOper except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) if _stream: - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: deserialized = _deserialize(_models.SecurityDomainOperationStatus, response.json()) @@ -147,6 +153,7 @@ async def _download_initial( } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = True pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -160,7 +167,10 @@ async def _download_initial( except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) response_headers = {} @@ -169,7 +179,7 @@ async def _download_initial( ) response_headers["Retry-After"] = self._deserialize("int", response.headers.get("Retry-After")) - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -287,6 +297,7 @@ async def get_upload_status(self, **kwargs: Any) -> _models.SecurityDomainOperat } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -301,11 +312,14 @@ async def get_upload_status(self, **kwargs: Any) -> _models.SecurityDomainOperat except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) if _stream: - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: deserialized = _deserialize(_models.SecurityDomainOperationStatus, response.json()) @@ -352,6 +366,7 @@ async def _upload_initial( } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = True pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -365,7 +380,10 @@ async def _upload_initial( except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) response_headers = {} @@ -375,7 +393,7 @@ async def _upload_initial( ) response_headers["Retry-After"] = self._deserialize("int", response.headers.get("Retry-After")) - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() if cls: return cls(pipeline_response, deserialized, response_headers) # type: ignore @@ -503,6 +521,7 @@ async def get_transfer_key(self, **kwargs: Any) -> _models.TransferKey: } _request.url = self._client.format_url(_request.url, **path_format_arguments) + _decompress = kwargs.pop("decompress", True) _stream = kwargs.pop("stream", False) pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access _request, stream=_stream, **kwargs @@ -517,11 +536,14 @@ async def get_transfer_key(self, **kwargs: Any) -> _models.TransferKey: except (StreamConsumedError, StreamClosedError): pass map_error(status_code=response.status_code, response=response, error_map=error_map) - error = _failsafe_deserialize(_models.KeyVaultError, response.json()) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) raise HttpResponseError(response=response, model=error) if _stream: - deserialized = response.iter_bytes() + deserialized = response.iter_bytes() if _decompress else response.iter_raw() else: deserialized = _deserialize(_models.TransferKey, response.json()) diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/__init__.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/__init__.py index 3e07fc9315ba..e998274a376a 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/__init__.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/__init__.py @@ -15,8 +15,8 @@ from ._models import ( # type: ignore CertificateInfo, - Error, KeyVaultError, + KeyVaultErrorError, SecurityDomain, SecurityDomainJsonWebKey, SecurityDomainOperationStatus, @@ -32,8 +32,8 @@ __all__ = [ "CertificateInfo", - "Error", "KeyVaultError", + "KeyVaultErrorError", "SecurityDomain", "SecurityDomainJsonWebKey", "SecurityDomainOperationStatus", diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/_models.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/_models.py index d945639fdcc8..9f80875cd870 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/_models.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/models/_models.py @@ -7,7 +7,7 @@ # -------------------------------------------------------------------------- # pylint: disable=useless-super-delegation -from typing import Any, List, Mapping, Optional, TYPE_CHECKING, Union, overload +from typing import Any, Mapping, Optional, TYPE_CHECKING, Union, overload from .._utils.model_base import Model as _Model, rest_field @@ -26,7 +26,7 @@ class CertificateInfo(_Model): :vartype required: int """ - certificates: List["_models.SecurityDomainJsonWebKey"] = rest_field( + certificates: list["_models.SecurityDomainJsonWebKey"] = rest_field( visibility=["read", "create", "update", "delete", "query"] ) """Certificates needed from customer. Required.""" @@ -38,7 +38,7 @@ class CertificateInfo(_Model): def __init__( self, *, - certificates: List["_models.SecurityDomainJsonWebKey"], + certificates: list["_models.SecurityDomainJsonWebKey"], required: Optional[int] = None, ) -> None: ... @@ -53,33 +53,33 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) -class Error(_Model): - """The key vault server error. +class KeyVaultError(_Model): + """The key vault error exception. + + :ivar error: The key vault server error. + :vartype error: ~azure.keyvault.securitydomain.models.KeyVaultErrorError + """ + + error: Optional["_models.KeyVaultErrorError"] = rest_field(visibility=["read"]) + """The key vault server error.""" + + +class KeyVaultErrorError(_Model): + """KeyVaultErrorError. :ivar code: The error code. :vartype code: str :ivar message: The error message. :vartype message: str :ivar inner_error: The key vault server error. - :vartype inner_error: ~azure.keyvault.securitydomain.models.Error + :vartype inner_error: ~azure.keyvault.securitydomain.models.KeyVaultErrorError """ code: Optional[str] = rest_field(visibility=["read"]) """The error code.""" message: Optional[str] = rest_field(visibility=["read"]) """The error message.""" - inner_error: Optional["_models.Error"] = rest_field(name="innererror", visibility=["read"]) - """The key vault server error.""" - - -class KeyVaultError(_Model): - """The key vault error exception. - - :ivar error: The key vault server error. - :vartype error: ~azure.keyvault.securitydomain.models.Error - """ - - error: Optional["_models.Error"] = rest_field(visibility=["read"]) + inner_error: Optional["_models.KeyVaultErrorError"] = rest_field(name="innererror", visibility=["read"]) """The key vault server error.""" @@ -146,13 +146,13 @@ class SecurityDomainJsonWebKey(_Model): `https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 `_. For Security Domain this value must be RSA. Required.""" - key_ops: List[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + key_ops: list[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) """Supported key operations. Required.""" n: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) """RSA modulus. Required.""" e: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) """RSA public exponent. Required.""" - x5_c: List[str] = rest_field(name="x5c", visibility=["read", "create", "update", "delete", "query"]) + x5_c: list[str] = rest_field(name="x5c", visibility=["read", "create", "update", "delete", "query"]) """X509 certificate chain parameter. Required.""" use: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) """Public Key Use Parameter. This is optional and if present must be enc.""" @@ -169,10 +169,10 @@ def __init__( *, kid: str, kty: str, - key_ops: List[str], + key_ops: list[str], n: str, e: str, - x5_c: List[str], + x5_c: list[str], x5_t_s256: str, alg: str, use: Optional[str] = None, diff --git a/sdk/keyvault/azure-keyvault-securitydomain/setup.py b/sdk/keyvault/azure-keyvault-securitydomain/setup.py new file mode 100644 index 000000000000..c4246f3c4831 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-securitydomain/setup.py @@ -0,0 +1,69 @@ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# -------------------------------------------------------------------------- + + +import os +import re +from setuptools import setup, find_packages + + +PACKAGE_NAME = "azure-keyvault-securitydomain" +PACKAGE_PPRINT_NAME = "Azure Keyvault Securitydomain" +PACKAGE_NAMESPACE = "azure.keyvault.securitydomain" + +# a.b.c => a/b/c +package_folder_path = PACKAGE_NAMESPACE.replace(".", "/") + +# Version extraction inspired from 'requests' +with open(os.path.join(package_folder_path, "_version.py"), "r") as fd: + version = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) + +if not version: + raise RuntimeError("Cannot find version information") + + +setup( + name=PACKAGE_NAME, + version=version, + description="Microsoft Corporation {} Client Library for Python".format(PACKAGE_PPRINT_NAME), + long_description=open("README.md", "r").read(), + long_description_content_type="text/markdown", + license="MIT License", + author="Microsoft Corporation", + author_email="azpysdkhelp@microsoft.com", + url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk", + keywords="azure, azure sdk", + classifiers=[ + "Development Status :: ", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", + ], + zip_safe=False, + packages=find_packages( + exclude=[ + "tests", + ] + ), + include_package_data=True, + package_data={ + "azure.keyvault.securitydomain": ["py.typed"], + }, + install_requires=[ + "isodate>=0.6.1", + "azure-core>=1.37.0", + "typing-extensions>=4.6.0", + ], + python_requires=">=3.9", +) diff --git a/sdk/keyvault/azure-keyvault-securitydomain/tests/jwe.py b/sdk/keyvault/azure-keyvault-securitydomain/tests/jwe.py index f756fa9ca0d9..8348a8c388c3 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/tests/jwe.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/tests/jwe.py @@ -42,15 +42,15 @@ def to_big_endian_64bits(value): @staticmethod def test_sp800_108(): - label = 'label' - context = 'context' + label = "label" + context = "context" bit_length = 256 - hex_result = 'f0ca51f6308791404bf68b56024ee7c64d6c737716f81d47e1e68b5c4e399575' + hex_result = "f0ca51f6308791404bf68b56024ee7c64d6c737716f81d47e1e68b5c4e399575" key = bytearray() key.extend([0x41] * 32) new_key = KDF.sp800_108(key, label, context, bit_length) - hex_value = new_key.hex().replace('-', '') # type: ignore + hex_value = new_key.hex().replace("-", "") # type: ignore return hex_value.lower() == hex_result @staticmethod @@ -88,9 +88,9 @@ def sp800_108(key_in: bytearray, label: str, context: str, bit_length): n += 1 hmac_data_suffix = bytearray() - hmac_data_suffix.extend(label.encode('UTF-8')) + hmac_data_suffix.extend(label.encode("UTF-8")) hmac_data_suffix.append(0) - hmac_data_suffix.extend(context.encode('UTF-8')) + hmac_data_suffix.extend(context.encode("UTF-8")) hmac_data_suffix.extend(KDF.to_big_endian_32bits(bit_length)) out_value = bytearray() @@ -105,18 +105,31 @@ def sp800_108(key_in: bytearray, label: str, context: str, bit_length): out_value.extend(hash_value) bytes_needed -= len(hash_value) else: - out_value.extend(hash_value[len(out_value): len(out_value) + bytes_needed]) + out_value.extend(hash_value[len(out_value) : len(out_value) + bytes_needed]) return out_value return None class JWEHeader: # pylint: disable=too-many-instance-attributes - _fields = ['alg', 'enc', 'zip', 'jku', 'jwk', 'kid', 'x5u', 'x5c', 'x5t', 'x5t_S256', 'typ', 'cty', 'crit'] - - def __init__(self, alg=None, enc=None, zip=None, # pylint: disable=redefined-builtin - jku=None, jwk=None, kid=None, x5u=None, x5c=None, x5t=None, - x5t_S256=None, typ=None, cty=None, crit=None): + _fields = ["alg", "enc", "zip", "jku", "jwk", "kid", "x5u", "x5c", "x5t", "x5t_S256", "typ", "cty", "crit"] + + def __init__( + self, + alg=None, + enc=None, + zip=None, # pylint: disable=redefined-builtin + jku=None, + jwk=None, + kid=None, + x5u=None, + x5c=None, + x5t=None, + x5t_S256=None, + typ=None, + cty=None, + crit=None, + ): """ JWE header @@ -153,8 +166,8 @@ def from_json_str(json_str): json_dict = json.loads(json_str) jwe_header = JWEHeader() for k in jwe_header._fields: - if k == 'x5t_S256': - v = json_dict.get('x5t#S256', None) + if k == "x5t_S256": + v = json_dict.get("x5t#S256", None) else: v = json_dict.get(k, None) if v is not None: @@ -166,8 +179,8 @@ def to_json_str(self): for k in self._fields: v = getattr(self, k, None) if v is not None: - if k == 'x5t_S256': - json_dict['x5t#S256'] = v + if k == "x5t_S256": + json_dict["x5t#S256"] = v else: json_dict[k] = v return json.dumps(json_dict) @@ -176,46 +189,46 @@ def to_json_str(self): class JWEDecode: def __init__(self, compact_jwe=None): if compact_jwe is None: - self.encoded_header = '' + self.encoded_header = "" self.encrypted_key = None self.init_vector = None self.ciphertext = None self.auth_tag = None self.protected_header = JWEHeader() else: - parts = compact_jwe.split('.') + parts = compact_jwe.split(".") self.encoded_header = parts[0] - header = base64.urlsafe_b64decode(self.encoded_header + '===').decode('ascii') # Fix incorrect padding + header = base64.urlsafe_b64decode(self.encoded_header + "===").decode("ascii") # Fix incorrect padding self.protected_header = JWEHeader.from_json_str(header) - self.encrypted_key = base64.urlsafe_b64decode(parts[1] + '===') - self.init_vector = base64.urlsafe_b64decode(parts[2] + '===') - self.ciphertext = base64.urlsafe_b64decode(parts[3] + '===') - self.auth_tag = base64.urlsafe_b64decode(parts[4] + '===') + self.encrypted_key = base64.urlsafe_b64decode(parts[1] + "===") + self.init_vector = base64.urlsafe_b64decode(parts[2] + "===") + self.ciphertext = base64.urlsafe_b64decode(parts[3] + "===") + self.auth_tag = base64.urlsafe_b64decode(parts[4] + "===") def encode_header(self): header_json = self.protected_header.to_json_str().replace('": ', '":').replace('", ', '",') - self.encoded_header = Utils.security_domain_b64_url_encode(header_json.encode('ascii')) + self.encoded_header = Utils.security_domain_b64_url_encode(header_json.encode("ascii")) def encode_compact(self): - ret = [self.encoded_header + '.'] + ret = [self.encoded_header + "."] if self.encrypted_key is not None: ret.append(Utils.security_domain_b64_url_encode(self.encrypted_key)) - ret.append('.') + ret.append(".") if self.init_vector is not None: ret.append(Utils.security_domain_b64_url_encode(self.init_vector)) - ret.append('.') + ret.append(".") if self.ciphertext is not None: ret.append(Utils.security_domain_b64_url_encode(self.ciphertext)) - ret.append('.') + ret.append(".") if self.auth_tag is not None: ret.append(Utils.security_domain_b64_url_encode(self.auth_tag)) - return ''.join(ret) + return "".join(ret) class JWE: @@ -228,26 +241,25 @@ def encode_compact(self): def get_padding_mode(self): alg = self.jwe_decode.protected_header.alg - if alg == 'RSA-OAEP-256': + if alg == "RSA-OAEP-256": algorithm = hashes.SHA256() return asymmetric_padding.OAEP( - mgf=asymmetric_padding.MGF1(algorithm=algorithm), algorithm=algorithm, label=None) + mgf=asymmetric_padding.MGF1(algorithm=algorithm), algorithm=algorithm, label=None + ) - if alg == 'RSA-OAEP': + if alg == "RSA-OAEP": algorithm = hashes.SHA1() return asymmetric_padding.OAEP( - mgf=asymmetric_padding.MGF1(algorithm=algorithm), algorithm=algorithm, label=None) + mgf=asymmetric_padding.MGF1(algorithm=algorithm), algorithm=algorithm, label=None + ) - if alg == 'RSA1_5': + if alg == "RSA1_5": return asymmetric_padding.PKCS1v15() return None def get_cek(self, private_key): - return private_key.decrypt( - self.jwe_decode.encrypted_key, - self.get_padding_mode() - ) + return private_key.decrypt(self.jwe_decode.encrypted_key, self.get_padding_mode()) def set_cek(self, cert, cek): public_key = cert.public_key() @@ -269,7 +281,7 @@ def hmac_key_from_cek(cek): def get_mac(self, hk): header_bytes = bytearray() - header_bytes.extend(self.jwe_decode.encoded_header.encode('ascii')) + header_bytes.extend(self.jwe_decode.encoded_header.encode("ascii")) auth_bits = len(header_bytes) * 8 hash_data = bytearray() @@ -289,7 +301,7 @@ def Aes256HmacSha512Decrypt(self, cek): test = 0 i = 0 while i < len(self.jwe_decode.auth_tag) == 32: # type: ignore - test |= (self.jwe_decode.auth_tag[i] ^ mac_value[i]) # type: ignore + test |= self.jwe_decode.auth_tag[i] ^ mac_value[i] # type: ignore i += 1 if test != 0: @@ -326,7 +338,7 @@ def Aes256HmacSha512Encrypt(self, cek, plaintext): self.jwe_decode.auth_tag.append(mac_value[i]) # type: ignore def decrypt_using_bytes(self, cek): - if self.jwe_decode.protected_header.enc == 'A256CBC-HS512': + if self.jwe_decode.protected_header.enc == "A256CBC-HS512": return self.Aes256HmacSha512Decrypt(cek) return None @@ -339,17 +351,17 @@ def decrypt_using_private_key(self, private_key): def encrypt_using_bytes(self, cek, plaintext, alg_id, kid=None): if kid is not None: - self.jwe_decode.protected_header.alg = 'dir' + self.jwe_decode.protected_header.alg = "dir" self.jwe_decode.protected_header.kid = kid - if alg_id == 'A256CBC-HS512': + if alg_id == "A256CBC-HS512": self.jwe_decode.protected_header.enc = alg_id self.jwe_decode.encode_header() self.Aes256HmacSha512Encrypt(cek, plaintext) def encrypt_using_cert(self, cert, plaintext): - self.jwe_decode.protected_header.alg = 'RSA-OAEP-256' - self.jwe_decode.protected_header.kid = 'not used' + self.jwe_decode.protected_header.alg = "RSA-OAEP-256" + self.jwe_decode.protected_header.kid = "not used" cek = Utils.get_random(64) self.set_cek(cert, cek) - self.encrypt_using_bytes(cek, plaintext, alg_id='A256CBC-HS512') + self.encrypt_using_bytes(cek, plaintext, alg_id="A256CBC-HS512") diff --git a/sdk/keyvault/azure-keyvault-securitydomain/tests/wrapping.py b/sdk/keyvault/azure-keyvault-securitydomain/tests/wrapping.py index 871f4e7c0511..3eb5f4da4015 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/tests/wrapping.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/tests/wrapping.py @@ -27,7 +27,7 @@ class ModMath: @staticmethod def reduce(x): - t = (x & 0xff) - (x >> 8) + t = (x & 0xFF) - (x >> 8) t += (t >> 31) & 257 return t @@ -64,7 +64,7 @@ def __init__(self, x, v): @staticmethod def from_uint16(w): x = w >> 9 - v = w & 0x1ff + v = w & 0x1FF return Share(x, v) def to_uint16(self): @@ -77,7 +77,7 @@ def __init__(self, required, secret_byte): @staticmethod def init_coefficients(required, secret_byte): - coefficients = array.array('H') + coefficients = array.array("H") for _ in range(required - 1): coefficients.append(ModMath.get_random()) coefficients.append(secret_byte) @@ -125,7 +125,7 @@ def __init__(self, shares=None, required=0): shares = 0 else: if shares > SharedSecret.max_shares or required > shares: - raise ValueError('Incorrect share or required count.') + raise ValueError("Incorrect share or required count.") self.shares = shares self.required = required @@ -147,7 +147,7 @@ def make_shares(self, plaintext): share_array = self.make_byte_shares(p) for sa in share_array: if i == 0: - share_arrays.append(array.array('H')) + share_arrays.append(array.array("H")) current_share_array = sa current_share_array.append(sa) return share_arrays @@ -162,7 +162,7 @@ def get_plaintext(share_arrays, required): plaintext_len = len(share_arrays[0]) for j in range(plaintext_len): - sv = array.array('H') + sv = array.array("H") for i in range(required): sa = share_arrays[i] sv.append(sa[j]) @@ -179,10 +179,7 @@ def __init__(self, enc_key=None, x5t_256=None): self.x5t_256 = x5t_256 def to_json(self): - return { - 'enc_key': self.enc_key if self.enc_key else '', - 'x5t_256': self.x5t_256 if self.x5t_256 else '' - } + return {"enc_key": self.enc_key if self.enc_key else "", "x5t_256": self.x5t_256 if self.x5t_256 else ""} class EncData: @@ -191,10 +188,7 @@ def __init__(self): self.kdf = None def to_json(self): - return { - 'data': [x.to_json() for x in self.data], - 'kdf': self.kdf if self.kdf else '' - } + return {"data": [x.to_json() for x in self.data], "kdf": self.kdf if self.kdf else ""} class Datum: @@ -203,10 +197,7 @@ def __init__(self, compact_jwe=None, tag=None): self.tag = tag def to_json(self): - return { - 'compact_jwe': self.compact_jwe if self.compact_jwe else '', - 'tag': self.tag if self.tag else '' - } + return {"compact_jwe": self.compact_jwe if self.compact_jwe else "", "tag": self.tag if self.tag else ""} class SecurityDomainRestoreData: @@ -215,10 +206,7 @@ def __init__(self): self.wrapped_key = Key() def to_json(self): - return { - 'EncData': self.enc_data.to_json(), - 'WrappedKey': self.wrapped_key.to_json() - } + return {"EncData": self.enc_data.to_json(), "WrappedKey": self.wrapped_key.to_json()} def _security_domain_gen_share_arrays(sd_wrapping_keys, shared_keys, required): @@ -230,21 +218,21 @@ def _security_domain_gen_share_arrays(sd_wrapping_keys, shared_keys, required): if ok: break - prefix = '.'.join(private_key_path.split('.')[:-1]) - cert_path = prefix + '.cer' + prefix = ".".join(private_key_path.split(".")[:-1]) + cert_path = prefix + ".cer" - with open(private_key_path, 'rb') as f: + with open(private_key_path, "rb") as f: pem_data = f.read() private_key = load_pem_private_key(pem_data, password=None, backend=default_backend()) - with open(cert_path, 'rb') as f: + with open(cert_path, "rb") as f: pem_data = f.read() cert = load_pem_x509_certificate(pem_data, backend=default_backend()) public_bytes = cert.public_bytes(Encoding.DER) x5tS256 = Utils.security_domain_b64_url_encode(hashlib.sha256(public_bytes).digest()) - for item in shared_keys['enc_shares']: - if x5tS256 == item['x5t_256']: - jwe = JWE(compact_jwe=item['enc_key']) + for item in shared_keys["enc_shares"]: + if x5tS256 == item["x5t_256"]: + jwe = JWE(compact_jwe=item["enc_key"]) share = jwe.decrypt_using_private_key(private_key) if not share: continue @@ -262,27 +250,27 @@ def _security_domain_gen_blob(transfer_key, share_arrays, enc_data, required): master_key = SharedSecret.get_plaintext(share_arrays, required=required) plaintext_list = [] - for item in enc_data['data']: - compact_jwe = item['compact_jwe'] - tag = item['tag'] - enc_key = KDF.sp800_108(master_key, label=tag, context='', bit_length=512) + for item in enc_data["data"]: + compact_jwe = item["compact_jwe"] + tag = item["tag"] + enc_key = KDF.sp800_108(master_key, label=tag, context="", bit_length=512) jwe_data = JWE(compact_jwe) plaintext = jwe_data.decrypt_using_bytes(enc_key) plaintext_list.append((plaintext, tag)) # encrypt security_domain_restore_data = SecurityDomainRestoreData() - security_domain_restore_data.enc_data.kdf = 'sp108_kdf' # type: ignore + security_domain_restore_data.enc_data.kdf = "sp108_kdf" # type: ignore master_key = Utils.get_random(32) for plaintext, tag in plaintext_list: - enc_key = KDF.sp800_108(master_key, label=tag, context='', bit_length=512) + enc_key = KDF.sp800_108(master_key, label=tag, context="", bit_length=512) jwe = JWE() - jwe.encrypt_using_bytes(enc_key, plaintext, alg_id='A256CBC-HS512', kid=tag) + jwe.encrypt_using_bytes(enc_key, plaintext, alg_id="A256CBC-HS512", kid=tag) datum = Datum(compact_jwe=jwe.encode_compact(), tag=tag) security_domain_restore_data.enc_data.data.append(datum) - with open(transfer_key, 'rb') as f: + with open(transfer_key, "rb") as f: pem_data = f.read() exchange_cert = load_pem_x509_certificate(pem_data, backend=default_backend()) @@ -304,22 +292,23 @@ def _security_domain_restore_blob(sd_file, transfer_key, sd_wrapping_keys): """Using the wrapping keys, prepare the security domain for upload.""" with open(sd_file) as f: sd_data = json.load(f) - if not sd_data or 'EncData' not in sd_data or 'SharedKeys' not in sd_data: - raise ValueError('Invalid SD file.') - enc_data = sd_data['EncData'] - shared_keys = sd_data['SharedKeys'] - required = shared_keys['required'] + if not sd_data or "EncData" not in sd_data or "SharedKeys" not in sd_data: + raise ValueError("Invalid SD file.") + enc_data = sd_data["EncData"] + shared_keys = sd_data["SharedKeys"] + required = shared_keys["required"] restore_blob_value = _security_domain_make_restore_blob( sd_wrapping_keys=sd_wrapping_keys, transfer_key=transfer_key, enc_data=enc_data, shared_keys=shared_keys, - required=required + required=required, ) return restore_blob_value + def use_wrapping_keys() -> dict: key_paths = [f"{CERT_PATH_PREFIX}0.pem", f"{CERT_PATH_PREFIX}1.pem"] blob_value = _security_domain_restore_blob(SECURITY_DOMAIN_PATH, TRANSFER_KEY_PATH, key_paths) - return {'value': blob_value} + return {"value": blob_value} diff --git a/sdk/keyvault/azure-keyvault-securitydomain/tsp-location.yaml b/sdk/keyvault/azure-keyvault-securitydomain/tsp-location.yaml index 7f1f278b6e39..07a7997d22f8 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/tsp-location.yaml +++ b/sdk/keyvault/azure-keyvault-securitydomain/tsp-location.yaml @@ -1,5 +1,3 @@ -directory: specification/keyvault/Security.KeyVault.SecurityDomain -commit: 89e8dbb5b552204552be0e15bd5d708fe05384ed +directory: specification/keyvault/data-plane/SecurityDomain +commit: ad180e80d6beb0a6fc9503ed8aaf97762759caf6 repo: Azure/azure-rest-api-specs -additionalDirectories: -- specification/keyvault/Security.KeyVault.Common/ From 38048f4215de18ceca61fdcbdb90a7da2cd298ff Mon Sep 17 00:00:00 2001 From: Nicola Camillucci Date: Thu, 7 May 2026 16:33:41 +0100 Subject: [PATCH 2/3] Updated default version --- .../azure/keyvault/securitydomain/_patch.py | 7 +++++-- .../azure/keyvault/securitydomain/aio/_patch.py | 3 ++- .../azure-keyvault-securitydomain/tests/_test_case.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_patch.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_patch.py index 328611823d0c..1bf4609efa7a 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_patch.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_patch.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. @@ -43,10 +44,12 @@ class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Key Vault API versions supported by this package""" #: this is the default version + V2025_07_01 = "2025-07-01" + V7_6 = "7.6" V7_5 = "7.5" -DEFAULT_VERSION = ApiVersion.V7_5 +DEFAULT_VERSION = ApiVersion.V2025_07_01 _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False @@ -90,7 +93,7 @@ class SecurityDomainClient(KeyVaultClient): :mod:`azure.identity` :type credential: ~azure.core.credentials.TokenCredential - :keyword str api_version: The API version to use for this operation. Default value is "7.5". Note that overriding + :keyword str api_version: The API version to use for this operation. Default value is "2025-07-01". Note that overriding this default value may result in unsupported behavior. :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key Vault or Managed HSM domain. Defaults to True. diff --git a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_patch.py b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_patch.py index 90e38f70716d..2c664bb6e5da 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_patch.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/aio/_patch.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # coding=utf-8 # -------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. @@ -46,7 +47,7 @@ class SecurityDomainClient(KeyVaultClient): :mod:`azure.identity` :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword str api_version: The API version to use for this operation. Default value is "7.5". Note that overriding + :keyword str api_version: The API version to use for this operation. Default value is "2025-07-01". Note that overriding this default value may result in unsupported behavior. :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key Vault or Managed HSM domain. Defaults to True. diff --git a/sdk/keyvault/azure-keyvault-securitydomain/tests/_test_case.py b/sdk/keyvault/azure-keyvault-securitydomain/tests/_test_case.py index ce06541806f9..145e36b8ce3d 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/tests/_test_case.py +++ b/sdk/keyvault/azure-keyvault-securitydomain/tests/_test_case.py @@ -70,6 +70,6 @@ def create_client(self, hsm_url, **kwargs): def get_decorator(**kwargs): """returns a test decorator for test parameterization""" - versions = kwargs.pop("api_versions", None) or ["7.5"] + versions = kwargs.pop("api_versions", None) or ["2025-07-01"] params = [pytest.param(api_version) for api_version in versions] return params From 661be0de159378530ce5e50d6a5447795c0f8da0 Mon Sep 17 00:00:00 2001 From: Nicola Camillucci Date: Thu, 7 May 2026 16:33:52 +0100 Subject: [PATCH 3/3] Updated changelog --- sdk/keyvault/azure-keyvault-securitydomain/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/keyvault/azure-keyvault-securitydomain/CHANGELOG.md b/sdk/keyvault/azure-keyvault-securitydomain/CHANGELOG.md index 7c82c8616373..72d7e1a8952d 100644 --- a/sdk/keyvault/azure-keyvault-securitydomain/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-securitydomain/CHANGELOG.md @@ -4,12 +4,16 @@ ### Features Added +- Added support for service API version `2025-07-01` [#46782](https://github.com/Azure/azure-sdk-for-python/pull/46782) + ### Breaking Changes ### Bugs Fixed ### Other Changes +- Key Vault API version `2025-07-01` is now the default + ## 1.0.0b1 (2025-05-07) ### Features Added