diff --git a/sdk/monitor/azure-mgmt-monitorslis/CHANGELOG.md b/sdk/monitor/azure-mgmt-monitorslis/CHANGELOG.md index 0805c03a6b8d..a4e6caa59d58 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/CHANGELOG.md +++ b/sdk/monitor/azure-mgmt-monitorslis/CHANGELOG.md @@ -1,5 +1,16 @@ # Release History +## 1.0.0b2 (2026-05-23) + +### Features Added + + - Enum `SamplingType` added member `AVERAGE` + - Enum `SamplingType` added member `COUNT` + +### Breaking Changes + + - Deleted or renamed enum value `SamplingType.AVG` + ## 1.0.0b1 (2026-04-27) ### Other Changes diff --git a/sdk/monitor/azure-mgmt-monitorslis/_metadata.json b/sdk/monitor/azure-mgmt-monitorslis/_metadata.json index c23edb0b2d1f..9c05764f07f4 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/_metadata.json +++ b/sdk/monitor/azure-mgmt-monitorslis/_metadata.json @@ -3,8 +3,8 @@ "apiVersions": { "Microsoft.Monitor": "2025-03-01-preview" }, - "commit": "fa74a24ebfcd57429a95ef827b06eae0e639800a", + "commit": "8be8c75d9bb11ea95d8a7e251db74aa78b5cd76c", "repository_url": "https://github.com/Azure/azure-rest-api-specs", "typespec_src": "specification/monitoringservice/resource-manager/Microsoft.Monitor/Slis", - "emitterVersion": "0.61.3" + "emitterVersion": "0.62.1" } \ No newline at end of file diff --git a/sdk/monitor/azure-mgmt-monitorslis/apiview-properties.json b/sdk/monitor/azure-mgmt-monitorslis/apiview-properties.json index c201445ab32f..d446b972afdc 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/apiview-properties.json +++ b/sdk/monitor/azure-mgmt-monitorslis/apiview-properties.json @@ -43,5 +43,6 @@ "azure.mgmt.monitorslis.aio.operations.SlisOperations.delete": "Microsoft.Monitor.Slis.delete", "azure.mgmt.monitorslis.operations.SlisOperations.list_by_parent": "Microsoft.Monitor.Slis.listByParent", "azure.mgmt.monitorslis.aio.operations.SlisOperations.list_by_parent": "Microsoft.Monitor.Slis.listByParent" - } + }, + "CrossLanguageVersion": "42338712126b" } \ No newline at end of file diff --git a/sdk/monitor/azure-mgmt-monitorslis/assets.json b/sdk/monitor/azure-mgmt-monitorslis/assets.json new file mode 100644 index 000000000000..1e07b863c206 --- /dev/null +++ b/sdk/monitor/azure-mgmt-monitorslis/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "python", + "TagPrefix": "python/monitor/azure-mgmt-monitorslis", + "Tag": "python/monitor/azure-mgmt-monitorslis_3192c4a8f5" +} diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_client.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_client.py index f6e91cec5246..497d77d60f39 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_client.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_client.py @@ -7,8 +7,8 @@ # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, Optional, TYPE_CHECKING, cast -from typing_extensions import Self from azure.core.pipeline import policies from azure.core.rest import HttpRequest, HttpResponse @@ -21,6 +21,11 @@ from ._utils.serialization import Deserializer, Serializer from .operations import SlisOperations +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore + if TYPE_CHECKING: from azure.core import AzureClouds from azure.core.credentials import TokenCredential @@ -39,8 +44,9 @@ class MonitorSlisMgmtClient: None. :paramtype cloud_setting: ~azure.core.AzureClouds :keyword api_version: The API version to use for this operation. Known values are - "2025-03-01-preview". Default value is "2025-03-01-preview". Note that overriding this default - value may result in unsupported behavior. + "2025-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_configuration.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_configuration.py index 5706eb3eda2a..8b4cc50efa58 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_configuration.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_configuration.py @@ -32,8 +32,9 @@ class MonitorSlisMgmtClientConfiguration: # pylint: disable=too-many-instance-a None. :type cloud_setting: ~azure.core.AzureClouds :keyword api_version: The API version to use for this operation. Known values are - "2025-03-01-preview". Default value is "2025-03-01-preview". Note that overriding this default - value may result in unsupported behavior. + "2025-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_patch.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_patch.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/model_base.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/model_base.py index db24930fdca9..d725c55906d3 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/model_base.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/model_base.py @@ -23,14 +23,19 @@ from json import JSONEncoder import xml.etree.ElementTree as ET from collections.abc import MutableMapping -from typing_extensions import Self import isodate from azure.core.exceptions import DeserializationError from azure.core import CaseInsensitiveEnumMeta from azure.core.pipeline import PipelineResponse from azure.core.serialization import _Null + from azure.core.rest import HttpResponse +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + _LOGGER = logging.getLogger(__name__) __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] @@ -585,6 +590,239 @@ def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typin return _serialize(value, rf._format) +# ============================================================================ +# Fast-path scalar deserializer functions for rest_field(deserializer=...) +# These are referenced from rest_field declarations to bypass the generic +# _deserialize -> _deserialize_with_callable chain. +# Only simple/primitive types — no models or container types. +# ============================================================================ + + +def _xml_deser_str(value): + if isinstance(value, ET.Element): + return value.text or "" + return str(value) if value is not None else None + + +def _xml_deser_int(value): + if isinstance(value, ET.Element): + return int(value.text) if value.text else None + return int(value) if value is not None else None + + +def _xml_deser_float(value): + if isinstance(value, ET.Element): + return float(value.text) if value.text else None + return float(value) if value is not None else None + + +def _xml_deser_bool(value): + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + if text in (True, False): + return text + return text.lower() == "true" + + +# pylint: disable=docstring-missing-param +def _xml_deser_bytes(value): + """Deserialize bytes from XML (base64).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_bytes(text) + + +def _xml_deser_bytes_base64url(value): + """Deserialize bytes from XML (base64url).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_bytes_base64(text) + + +def _xml_deser_datetime(value): + """Deserialize a datetime from XML (ISO 8601 / rfc3339).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_datetime(text) + + +def _xml_deser_datetime_rfc7231(value): + """Deserialize a datetime from XML (RFC7231 format).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_datetime_rfc7231(text) + + +def _xml_deser_datetime_unix_timestamp(value): + """Deserialize a datetime from XML (Unix timestamp).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_datetime_unix_timestamp(float(text)) + + +def _xml_deser_date(value): + """Deserialize a date from XML (ISO 8601).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_date(text) + + +def _xml_deser_time(value): + """Deserialize a time from XML (ISO 8601).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_time(text) + + +def _xml_deser_duration(value): + """Deserialize a timedelta from XML (ISO 8601 duration).""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_duration(text) + + +def _xml_deser_decimal(value): + """Deserialize a Decimal from XML.""" + if isinstance(value, ET.Element): + text = value.text + else: + text = value + if text is None: + return None + return _deserialize_decimal(text) + + +def _xml_deser_enum_or_str(enum_cls, value): + """Deserialize a Union[EnumType, str] from XML.""" + text = value.text if isinstance(value, ET.Element) else value + if text is None: + return None + try: + return enum_cls(text) + except ValueError: + return text + + +def _extract_xml_model_type(rf_type): + """Extract the concrete Model class from a resolved rf._type partial chain. + + Unwraps ``Optional[Model]`` and ``_deserialize_model(Model, ...)`` + wrappers. Only handles Model and Optional[Model] — other composite + types (List, Dict, Union, etc.) return None and fall through to the + generic ``_deserialize`` path at runtime. + """ + if rf_type is None: + return None + if isinstance(rf_type, type) and _is_model(rf_type): + return rf_type + if not isinstance(rf_type, functools.partial): + return None + func = rf_type.func + args = rf_type.args + if func is _deserialize_with_optional and args: + return _extract_xml_model_type(args[0]) + if func is _deserialize_model and args: + cls = args[0] + return cls if isinstance(cls, type) and _is_model(cls) else None + return None + + +def _build_xml_field_plan( # pylint: disable=docstring-missing-return, docstring-missing-rtype, unused-variable + cls, attr_to_rest_field: dict +) -> list: + """Build a precomputed XML field plan for fast _init_from_xml iteration. + + Called once per model class in __new__. Returns a list of tuples: + (rest_name, xml_name, kind, deser, rf_type, is_optional, items_name) + + kind: 0=wrapped, 1=attribute, 2=unwrapped, 3=text + + For Model and Optional[Model] fields that lack a scalar + ``_deserializer``, this function precomputes the Model class as the + deserializer so ``_init_from_xml`` can call ``ModelClass(element)`` + directly instead of going through the expensive + ``_get_deserialize_callable_from_annotation`` chain at runtime. + """ + model_meta = getattr(cls, "_xml", {}) + model_ns = model_meta.get("ns") or model_meta.get("namespace") + plan = [] + + for rf in attr_to_rest_field.values(): + prop_meta = getattr(rf, "_xml", {}) + deser = rf._deserializer + + 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 + + is_optional = rf._is_optional + + # For Model / Optional[Model] fields without a scalar deserializer, + # precompute the Model class as the deserializer. + if deser is None and rf._type is not None: + model_cls = _extract_xml_model_type(rf._type) + if model_cls is not None: + deser = model_cls + + if prop_meta.get("attribute", False): + plan.append((rf._rest_name, xml_name, 1, deser, rf._type, is_optional, None)) + elif prop_meta.get("unwrapped", False): + items_name = prop_meta.get("itemsName") + if items_name: + items_ns = prop_meta.get("itemsNs") + if items_ns is not None: + xml_ns = items_ns + if xml_ns: + items_name = "{" + xml_ns + "}" + items_name + else: + items_name = xml_name + plan.append((rf._rest_name, xml_name, 2, deser, rf._type, is_optional, items_name)) + elif prop_meta.get("text", False): + plan.append((rf._rest_name, xml_name, 3, deser, rf._type, is_optional, None)) + else: + plan.append((rf._rest_name, xml_name, 0, deser, rf._type, is_optional, None)) + + return plan + + +# pylint: enable=docstring-missing-param class Model(_MyMutableMapping): _is_model = True # label whether current class's _attr_to_rest_field has been calculated @@ -595,11 +833,7 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: class_name = self.__class__.__name__ if len(args) > 1: raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") - dict_to_pass = { - rest_field._rest_name: rest_field._default - for rest_field in self._attr_to_rest_field.values() - if rest_field._default is not _UNSET - } + dict_to_pass: dict[str, typing.Any] = {} if args: if isinstance(args[0], ET.Element): dict_to_pass.update(self._init_from_xml(args[0])) @@ -619,9 +853,19 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: if v is not None } ) + # Apply client default values for fields the caller didn't set so that + # defaults are part of `_data` and therefore included during serialization. + for rf in self._attr_to_rest_field.values(): + if rf._default is _UNSET: + continue + if rf._rest_name in dict_to_pass: + continue + dict_to_pass[rf._rest_name] = _create_value(rf, rf._default) super().__init__(dict_to_pass) - def _init_from_xml(self, element: ET.Element) -> dict[str, typing.Any]: + def _init_from_xml( # pylint: disable=too-many-branches, too-many-statements + 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. @@ -629,53 +873,89 @@ def _init_from_xml(self, element: ET.Element) -> dict[str, typing.Any]: :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: + field_plan = getattr(self, "_xml_field_plan", None) + if field_plan: + for rest_name, xml_name, kind, deser, rf_type, is_optional, items_name in field_plan: + if kind == 0: # wrapped element (most common) + item = element.find(xml_name) + if item is not None: + existed_attr_keys.append(xml_name) + if deser: + result[rest_name] = deser(item) + else: + result[rest_name] = _deserialize(rf_type, item) + elif kind == 1: # attribute + attr_val = element.get(xml_name) + if attr_val is not None: + existed_attr_keys.append(xml_name) + if deser: + result[rest_name] = deser(attr_val) + else: + result[rest_name] = attr_val + elif kind == 2: # unwrapped array + items = element.findall(items_name) # pyright: ignore + if len(items) > 0: + existed_attr_keys.append(items_name) + if deser: + result[rest_name] = deser(items) + else: + result[rest_name] = _deserialize(rf_type, items) + elif not is_optional: + existed_attr_keys.append(items_name) + result[rest_name] = [] + elif kind == 3: # text + if element.text is not None: + if deser: + result[rest_name] = deser(element.text) + else: + result[rest_name] = element.text + else: + 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 = _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, items) - elif not rf._is_optional: + result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name)) + continue + + # unwrapped element is array + if prop_meta.get("unwrapped", False): + _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 + item = element.find(xml_name) + if item is not None: 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) + result[rf._rest_name] = _deserialize(rf._type, item) # rest thing is additional properties for e in element: @@ -708,6 +988,9 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: if not rf._rest_name_input: rf._rest_name_input = attr cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items()) + # Build XML field plan for fast _init_from_xml (only for XML models) + if getattr(cls, "_xml", None): + cls._xml_field_plan = _build_xml_field_plan(cls, attr_to_rest_field) cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") return super().__new__(cls) @@ -1082,6 +1365,7 @@ def __init__( format: typing.Optional[str] = None, is_multipart_file_input: bool = False, xml: typing.Optional[dict[str, typing.Any]] = None, + deserializer: typing.Optional[typing.Callable] = None, ): self._type = type self._rest_name_input = name @@ -1094,6 +1378,7 @@ def __init__( self._format = format self._is_multipart_file_input = is_multipart_file_input self._xml = xml if xml is not None else {} + self._deserializer = deserializer @property def _class_type(self) -> typing.Any: @@ -1113,7 +1398,10 @@ 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 # Use _data.get() directly to avoid triggering __getitem__ which clears the cache - item = obj._data.get(self._rest_name) + item = obj._data.get(self._rest_name, _UNSET) + if item is _UNSET: + # Field not set by user; return the client default if one exists, otherwise None + return self._default if self._default is not _UNSET else None if item is None: return item if self._is_model: @@ -1126,7 +1414,11 @@ def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin # 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) + # Fast path: use _deserializer directly (avoids _serialize/_deserialize chain) + if self._deserializer: + deserialized = self._deserializer(item) + else: + 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 @@ -1172,6 +1464,7 @@ def rest_field( format: typing.Optional[str] = None, is_multipart_file_input: bool = False, xml: typing.Optional[dict[str, typing.Any]] = None, + deserializer: typing.Optional[typing.Callable] = None, ) -> typing.Any: return _RestField( name=name, @@ -1181,6 +1474,7 @@ def rest_field( format=format, is_multipart_file_input=is_multipart_file_input, xml=xml, + deserializer=deserializer, ) @@ -1414,6 +1708,8 @@ def _deserialize_xml( value: str, ) -> typing.Any: element = ET.fromstring(value) # nosec + if _is_model(deserializer): + return deserializer._deserialize(element, []) return _deserialize(deserializer, element) diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/serialization.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/serialization.py index 81ec1de5922b..a088671e9c51 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/serialization.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_utils/serialization.py @@ -39,11 +39,15 @@ import xml.etree.ElementTree as ET import isodate # type: ignore -from typing_extensions import Self from azure.core.exceptions import DeserializationError, SerializationError from azure.core.serialization import NULL as CoreNull +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + _BOM = codecs.BOM_UTF8.decode(encoding="utf-8") JSON = MutableMapping[str, Any] @@ -1401,7 +1405,7 @@ def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: # Otherwise, result are unexpected self.additional_properties_detection = True - def __call__(self, target_obj, response_data, content_type=None): + def __call__(self, target_obj, response_data, content_type=None): # pylint: disable=too-many-return-statements """Call the deserializer to process a REST response. :param str target_obj: Target data type to deserialize to. @@ -1411,6 +1415,27 @@ def __call__(self, target_obj, response_data, content_type=None): :return: Deserialized object. :rtype: object """ + # Fast path for header deserialization: response_data is a plain str or None + # and target_obj is a simple scalar type. This avoids the expensive + # _unpack_content → _deserialize → _classify_target → deserialize_data chain. + if response_data is None: + return None + if target_obj == "str" and isinstance(response_data, str): + return response_data + if isinstance(response_data, str): + if target_obj == "int": + return int(response_data) + if target_obj == "bool": + if response_data in ("true", "1", "True"): + return True + if response_data in ("false", "0", "False"): + return False + return bool(response_data) + if target_obj == "rfc-1123": + return Deserializer.deserialize_rfc(response_data) + if target_obj == "bytearray": + return Deserializer.deserialize_bytearray(response_data) + data = self._unpack_content(response_data, content_type) return self._deserialize(target_obj, data) diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_version.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_version.py index be71c81bd282..bbcd28b4aa67 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_version.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "1.0.0b1" +VERSION = "1.0.0b2" diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_client.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_client.py index e035ce2da475..d8aeabc5e241 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_client.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_client.py @@ -7,8 +7,8 @@ # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, Awaitable, Optional, TYPE_CHECKING, cast -from typing_extensions import Self from azure.core.pipeline import policies from azure.core.rest import AsyncHttpResponse, HttpRequest @@ -21,6 +21,11 @@ from ._configuration import MonitorSlisMgmtClientConfiguration from .operations import SlisOperations +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore + if TYPE_CHECKING: from azure.core import AzureClouds from azure.core.credentials_async import AsyncTokenCredential @@ -39,8 +44,9 @@ class MonitorSlisMgmtClient: None. :paramtype cloud_setting: ~azure.core.AzureClouds :keyword api_version: The API version to use for this operation. Known values are - "2025-03-01-preview". Default value is "2025-03-01-preview". Note that overriding this default - value may result in unsupported behavior. + "2025-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_configuration.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_configuration.py index 9d2ee0c203ea..03ad54f0da7e 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_configuration.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_configuration.py @@ -32,8 +32,9 @@ class MonitorSlisMgmtClientConfiguration: # pylint: disable=too-many-instance-a None. :type cloud_setting: ~azure.core.AzureClouds :keyword api_version: The API version to use for this operation. Known values are - "2025-03-01-preview". Default value is "2025-03-01-preview". Note that overriding this default - value may result in unsupported behavior. + "2025-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_patch.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_patch.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_operations.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_operations.py index fd4be66ceef0..220697d00703 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_operations.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_operations.py @@ -397,7 +397,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "endpoint": self._serialize.url( diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_patch.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_patch.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/aio/operations/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_enums.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_enums.py index 6b8557fe4103..591078104051 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_enums.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_enums.py @@ -22,29 +22,31 @@ class Category(str, Enum, metaclass=CaseInsensitiveEnumMeta): class ConditionOperator(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Defines operators used in filter conditions.""" - EQUAL = "==" + EQUAL = "eq" """Equal to.""" - NOT_EQUAL = "!=" + NOT_EQUAL = "ne" """Not equal to.""" - LESS_THAN = "<" + LESS_THAN = "lt" """Less than.""" - LESS_THAN_OR_EQUAL = "<=" + LESS_THAN_OR_EQUAL = "lte" """Less than or equal to.""" - GREATER_THAN = ">" + GREATER_THAN = "gt" """Greater than.""" - GREATER_THAN_OR_EQUAL = ">=" + GREATER_THAN_OR_EQUAL = "gte" """Greater than or equal to.""" - IN = "@in" - """In operator.""" - NOT_IN = "!in" - """Not in.""" + IN = "in" + """Matches when ``value`` is one of the items in the ``^^``-delimited list (for example, ``value`` + = "east^^west^^north").""" + NOT_IN = "notin" + """Matches when ``value`` is none of the items in the ``^^``-delimited list (for example, + ``value`` = "east^^west^^north").""" STARTS_WITH = "startswith" """Starts with.""" - NOT_STARTS_WITH = "!startswith" + NOT_STARTS_WITH = "notstartswith" """Does not start with.""" CONTAINS = "contains" """Contains the value.""" - NOT_CONTAINS = "!contains" + NOT_CONTAINS = "notcontains" """Does not contain the value.""" @@ -108,14 +110,16 @@ class ProvisioningState(str, Enum, metaclass=CaseInsensitiveEnumMeta): class SamplingType(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Defines the available sampling types.""" - MAX = "max" - """Maximum value.""" - MIN = "min" - """Minimum value.""" - AVG = "avg" + AVERAGE = "Average" """Average value.""" - SUM = "sum" + SUM = "Sum" """Summation.""" + COUNT = "Count" + """Count of occurrences.""" + MIN = "Min" + """Minimum value.""" + MAX = "Max" + """Maximum value.""" class ScalarFunction(str, Enum, metaclass=CaseInsensitiveEnumMeta): @@ -176,11 +180,11 @@ class TemporalAggregationType(str, Enum, metaclass=CaseInsensitiveEnumMeta): class WindowUptimeCriteriaComparator(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Defines comparison operators for window uptime criteria.""" - LESS_THAN = "<" + LESS_THAN = "lt" """Less than the target value.""" - GREATER_THAN = ">" + GREATER_THAN = "gt" """Greater than the target value.""" - LESS_THAN_OR_EQUAL = "<=" + LESS_THAN_OR_EQUAL = "lte" """Less than or equal to the target value.""" - GREATER_THAN_OR_EQUAL = ">=" + GREATER_THAN_OR_EQUAL = "gte" """Greater than or equal to the target value.""" diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_models.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_models.py index 19036802bb73..2acf5bd73ab4 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_models.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_models.py @@ -137,14 +137,17 @@ class Condition(_Model): :ivar scalar_function: Scalar function applied for filtering. Known values are: "max", "min", "avg", and "sum". :vartype scalar_function: str or ~azure.mgmt.monitorslis.models.ScalarFunction - :ivar sampling_type: Defines the sampling type. Known values are: "max", "min", "avg", and - "sum". + :ivar sampling_type: Defines the sampling type. Known values are: "Average", "Sum", "Count", + "Min", and "Max". :vartype sampling_type: str or ~azure.mgmt.monitorslis.models.SamplingType - :ivar operator: Operator used in the filtering condition. Required. Known values are: "==", - "!=", "<", "<=", ">", ">=", "@in", "!in", "startswith", "!startswith", "contains", and - "!contains". + :ivar operator: Operator used in the filtering condition. Required. Known values are: "eq", + "ne", "lt", "lte", "gt", "gte", "in", "notin", "startswith", "notstartswith", "contains", and + "notcontains". :vartype operator: str or ~azure.mgmt.monitorslis.models.ConditionOperator - :ivar value: Value used in filtering. Required. + :ivar value: Value used in filtering. For most operators (eq, ne, lt, lte, gt, gte, startswith, + notstartswith, contains, notcontains) this is a single value (for example "GetContosoUsers"). + For the ``in`` and ``notin`` operators, multiple values must be joined by the delimiter ``^^`` + (for example "east^^west^^north"). Required. :vartype value: str """ @@ -160,15 +163,19 @@ class Condition(_Model): sampling_type: Optional[Union[str, "_models.SamplingType"]] = rest_field( name="samplingType", visibility=["read", "create", "update", "delete", "query"] ) - """Defines the sampling type. Known values are: \"max\", \"min\", \"avg\", and \"sum\".""" + """Defines the sampling type. Known values are: \"Average\", \"Sum\", \"Count\", \"Min\", and + \"Max\".""" operator: Union[str, "_models.ConditionOperator"] = rest_field( visibility=["read", "create", "update", "delete", "query"] ) - """Operator used in the filtering condition. Required. Known values are: \"==\", \"!=\", \"<\", - \"<=\", \">\", \">=\", \"@in\", \"!in\", \"startswith\", \"!startswith\", \"contains\", and - \"!contains\".""" + """Operator used in the filtering condition. Required. Known values are: \"eq\", \"ne\", \"lt\", + \"lte\", \"gt\", \"gte\", \"in\", \"notin\", \"startswith\", \"notstartswith\", \"contains\", + and \"notcontains\".""" value: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) - """Value used in filtering. Required.""" + """Value used in filtering. For most operators (eq, ne, lt, lte, gt, gte, startswith, + notstartswith, contains, notcontains) this is a single value (for example \"GetContosoUsers\"). + For the ``in`` and ``notin`` operators, multiple values must be joined by the delimiter ``^^`` + (for example \"east^^west^^north\"). Required.""" @overload def __init__( @@ -902,7 +909,7 @@ class WindowUptimeCriteria(_Model): :ivar target: Threshold value used to determine uptime. Required. :vartype target: float :ivar comparator: Comparison operator used for uptime evaluation. Required. Known values are: - "<", ">", "<=", and ">=". + "lt", "gt", "lte", and "gte". :vartype comparator: str or ~azure.mgmt.monitorslis.models.WindowUptimeCriteriaComparator """ @@ -911,8 +918,8 @@ class WindowUptimeCriteria(_Model): comparator: Union[str, "_models.WindowUptimeCriteriaComparator"] = rest_field( visibility=["read", "create", "update", "delete", "query"] ) - """Comparison operator used for uptime evaluation. Required. Known values are: \"<\", \">\", - \"<=\", and \">=\".""" + """Comparison operator used for uptime evaluation. Required. Known values are: \"lt\", \"gt\", + \"lte\", and \"gte\".""" @overload def __init__( diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_patch.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_patch.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/models/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_operations.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_operations.py index 609fef11af0c..1aad5236d07e 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_operations.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_operations.py @@ -491,7 +491,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "endpoint": self._serialize.url( diff --git a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_patch.py b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_patch.py index 87676c65a8f0..ea765788358a 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_patch.py +++ b/sdk/monitor/azure-mgmt-monitorslis/azure/mgmt/monitorslis/operations/_patch.py @@ -8,7 +8,6 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ - __all__: list[str] = [] # Add all objects you want publicly available to users at this package level diff --git a/sdk/monitor/azure-mgmt-monitorslis/generated_samples/slis_create_or_update.py b/sdk/monitor/azure-mgmt-monitorslis/generated_samples/slis_create_or_update.py index aee866be1db9..46f531fcd42a 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/generated_samples/slis_create_or_update.py +++ b/sdk/monitor/azure-mgmt-monitorslis/generated_samples/slis_create_or_update.py @@ -53,7 +53,7 @@ def main(): "signalFormula": "(A + B) /2", "signalSources": [ { - "filters": [{"dimensionName": "ApiName", "operator": "==", "value": "GetContosoUsers"}], + "filters": [{"dimensionName": "ApiName", "operator": "eq", "value": "GetContosoUsers"}], "metricName": "Stamp1Latency", "metricNamespace": "ContosoMetricsWest", "signalSourceId": "A", @@ -63,7 +63,7 @@ def main(): "temporalAggregation": {"type": "Average", "windowSizeMinutes": 5}, }, { - "filters": [{"dimensionName": "ApiName", "operator": "==", "value": "GetContosoUsers"}], + "filters": [{"dimensionName": "ApiName", "operator": "eq", "value": "GetContosoUsers"}], "metricName": "Stamp2Latency", "metricNamespace": "ContosoMetricsEast", "signalSourceId": "B", @@ -74,7 +74,7 @@ def main(): }, ], }, - "windowUptimeCriteria": {"comparator": ">=", "target": 95}, + "windowUptimeCriteria": {"comparator": "gte", "target": 95}, }, } }, diff --git a/sdk/monitor/azure-mgmt-monitorslis/pyproject.toml b/sdk/monitor/azure-mgmt-monitorslis/pyproject.toml index 0435778f6010..73d68d815262 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/pyproject.toml +++ b/sdk/monitor/azure-mgmt-monitorslis/pyproject.toml @@ -17,13 +17,12 @@ classifiers = [ "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", ] -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = [ "azure", "azure sdk", diff --git a/sdk/monitor/azure-mgmt-monitorslis/test-resources.bicep b/sdk/monitor/azure-mgmt-monitorslis/test-resources.bicep new file mode 100644 index 000000000000..e3104bdd3c36 --- /dev/null +++ b/sdk/monitor/azure-mgmt-monitorslis/test-resources.bicep @@ -0,0 +1,60 @@ +// test-resources.bicep for Microsoft.Monitor/slis live tests +// Provisions: +// - Azure Monitor Workspace (destination for SLI metrics) +// - User-Assigned Managed Identity (used by SLI signal sources) +// - Role assignment for the test application SP + +@description('The location of the resource. By default, this is the same as the resource group.') +param location string = resourceGroup().location + +@description('The client OID to grant access to test resources.') +param testApplicationOid string + +@description('The base resource name.') +param baseName string = resourceGroup().name + +// Azure Monitor Workspace (destination AMW account for the SLI) +resource monitorWorkspace 'Microsoft.Monitor/accounts@2023-04-03' = { + name: '${baseName}-amw' + location: location + properties: {} +} + +// User-Assigned Managed Identity (used as signal source identity) +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${baseName}-sli-id' + location: location +} + +// Monitoring Reader role (43d0d8ad-25c7-4714-9337-8ba259a9fe05) +// Grants the test SP read access needed for SLI operations +var monitoringReaderRoleId = '43d0d8ad-25c7-4714-9337-8ba259a9fe05' + +resource monitoringReaderAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, testApplicationOid, monitoringReaderRoleId) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', monitoringReaderRoleId) + principalId: testApplicationOid + principalType: 'ServicePrincipal' + } +} + +// Monitoring Contributor role (749f88d5-cbae-40b8-bcfc-e573ddc772fa) +// Grants the test SP write access for SLI CRUD operations +var monitoringContributorRoleId = '749f88d5-cbae-40b8-bcfc-e573ddc772fa' + +resource monitoringContributorAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, testApplicationOid, monitoringContributorRoleId) + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', monitoringContributorRoleId) + principalId: testApplicationOid + principalType: 'ServicePrincipal' + } +} + +// OUTPUTS - these become environment variables for the test runner +output AMW_RESOURCE_ID string = monitorWorkspace.id +output MANAGED_IDENTITY_RESOURCE_ID string = managedIdentity.id +output SOURCE_AMW_RESOURCE_ID string = monitorWorkspace.id +output SOURCE_MANAGED_IDENTITY_RESOURCE_ID string = managedIdentity.id +output SERVICE_GROUP_NAME string = 'testSG' diff --git a/sdk/monitor/azure-mgmt-monitorslis/tests/.env.sample b/sdk/monitor/azure-mgmt-monitorslis/tests/.env.sample new file mode 100644 index 000000000000..42b0abc0804b --- /dev/null +++ b/sdk/monitor/azure-mgmt-monitorslis/tests/.env.sample @@ -0,0 +1,15 @@ +# Environment variables for SLI live tests +# Copy this to .env and fill in real values for local testing + +# Azure credentials (set by DefaultAzureCredential or the test framework) +AZURE_TENANT_ID= +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_SUBSCRIPTION_ID= + +# SLI test-specific variables (provisioned by test-resources.bicep) +SERVICE_GROUP_NAME=arm-sdk-tests-sg +AMW_RESOURCE_ID=/subscriptions/6820e35f-0fe6-4af3-aad2-27414fa82621/resourceGroups/mfrei/providers/microsoft.monitor/accounts/streaming-3p-slo-am2cbn-eastus2euap-1 +MANAGED_IDENTITY_RESOURCE_ID=/subscriptions/6820e35f-0fe6-4af3-aad2-27414fa82621/resourceGroups/mfrei/providers/Microsoft.ManagedIdentity/userAssignedIdentities/mfrei-test-user-managed-identity +SOURCE_AMW_RESOURCE_ID=/subscriptions/6820e35f-0fe6-4af3-aad2-27414fa82621/resourceGroups/mfrei/providers/microsoft.monitor/accounts/streaming-3p-slo-am2cbn-eastus2euap-1 +SOURCE_MANAGED_IDENTITY_RESOURCE_ID=/subscriptions/6820e35f-0fe6-4af3-aad2-27414fa82621/resourceGroups/mfrei/providers/Microsoft.ManagedIdentity/userAssignedIdentities/mfrei-test-user-managed-identity diff --git a/sdk/monitor/azure-mgmt-monitorslis/tests/test_sli_crud_live.py b/sdk/monitor/azure-mgmt-monitorslis/tests/test_sli_crud_live.py new file mode 100644 index 000000000000..fda42dc1342a --- /dev/null +++ b/sdk/monitor/azure-mgmt-monitorslis/tests/test_sli_crud_live.py @@ -0,0 +1,150 @@ +# 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. +# -------------------------------------------------------------------------- +import os +import pytest +from azure.core.exceptions import ResourceNotFoundError +from azure.mgmt.monitorslis import MonitorSlisMgmtClient, models + +from devtools_testutils import AzureMgmtRecordedTestCase, recorded_by_proxy + + +# Environment variables expected: +# SERVICE_GROUP_NAME - name of the pre-existing Service Group +# AMW_RESOURCE_ID - Azure Monitor Workspace resource ID (destination) +# MANAGED_IDENTITY_RESOURCE_ID - User-Assigned Managed Identity resource ID +# SOURCE_AMW_RESOURCE_ID - Source Azure Monitor Workspace resource ID +# SOURCE_MANAGED_IDENTITY_RESOURCE_ID - Source Managed Identity resource ID +SERVICE_GROUP_NAME = os.environ.get("SERVICE_GROUP_NAME", "arm-sdk-tests-sg") +AMW_RESOURCE_ID = os.environ.get( + "AMW_RESOURCE_ID", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/arm-sdk-tests-rg/providers/microsoft.monitor/accounts/amw-arm-sdk-tests-rg", +) +MANAGED_IDENTITY_RESOURCE_ID = os.environ.get( + "MANAGED_IDENTITY_RESOURCE_ID", + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/arm-sdk-tests-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uami-arm-sdk-tests-rg", +) +SOURCE_AMW_RESOURCE_ID = os.environ.get("SOURCE_AMW_RESOURCE_ID", AMW_RESOURCE_ID) +SOURCE_MANAGED_IDENTITY_RESOURCE_ID = os.environ.get( + "SOURCE_MANAGED_IDENTITY_RESOURCE_ID", MANAGED_IDENTITY_RESOURCE_ID +) + +SLI_DESCRIPTION = "Live test SLI - measures latency of test API" + + +class TestSliCrudLive(AzureMgmtRecordedTestCase): + """Live CRUD test for Microsoft.Monitor/slis resource.""" + + def setup_method(self, method): + self.client = self.create_mgmt_client(MonitorSlisMgmtClient) + self.sli_name = self.get_resource_name("pysli") + + def _get_sli_body(self): + """Return a valid SLI resource body for create/update.""" + identities = {MANAGED_IDENTITY_RESOURCE_ID: models.UserAssignedIdentity()} + if SOURCE_MANAGED_IDENTITY_RESOURCE_ID.lower() != MANAGED_IDENTITY_RESOURCE_ID.lower(): + identities[SOURCE_MANAGED_IDENTITY_RESOURCE_ID] = models.UserAssignedIdentity() + return models.Sli( + identity=models.ManagedServiceIdentity( + type="UserAssigned", + user_assigned_identities=identities, + ), + properties=models.SliResource( + description=SLI_DESCRIPTION, + category="Latency", + evaluation_type="WindowBased", + enable_alert=True, + destination_amw_accounts=[ + models.AmwAccount( + resource_id=AMW_RESOURCE_ID, + identity=MANAGED_IDENTITY_RESOURCE_ID, + ) + ], + baseline_properties=models.BaselineProperties( + baseline=models.Baseline( + value=99, + evaluation_period_days=30, + evaluation_calculation_type="CalendarDays", + ) + ), + sli_properties=models.SliProperties( + window_uptime_criteria=models.WindowUptimeCriteria(target=95, comparator="gte"), + signals=models.Signal( + signal_sources=[ + models.SignalSource( + signal_source_id="A", + source_amw_account_managed_identity=SOURCE_MANAGED_IDENTITY_RESOURCE_ID, + source_amw_account_resource_id=SOURCE_AMW_RESOURCE_ID, + # Source metric is a real Azure Managed Prometheus metric scraped by AKS. + # Test infra (bicep) deploys an AKS cluster with the Azure Monitor metrics addon + # pointed at the source AMW; container_cpu_usage_seconds_total is always populated. + metric_namespace="customdefault", + metric_name="container_cpu_usage_seconds_total", + filters=[ + models.Condition( + dimension_name="container", + # Use wire value "ne" directly (the generated ConditionOperator + # enum has incorrect values — tracked separately). + operator="ne", + value="POD", + ) + ], + spatial_aggregation=models.SpatialAggregation( + type="Sum", + dimensions=["instance"], + ), + temporal_aggregation=models.TemporalAggregation( + type="Rate", + window_size_minutes=1, + ), + ) + ], + signal_formula="A", + ), + ), + ) + ) + + @recorded_by_proxy + def test_sli_crud_lifecycle(self): + """Test full CRUD lifecycle: Create → Get → Delete → Get (expect 404).""" + # Step 1: Create SLI + create_response = self.client.slis.create_or_update( + service_group_name=SERVICE_GROUP_NAME, + sli_name=self.sli_name, + resource=self._get_sli_body(), + ) + assert create_response is not None + # In playback the recording's response name is sanitized to "Sanitized"; + # in live mode it should equal self.sli_name. Allow both. + assert create_response.name in (self.sli_name, "Sanitized") + assert create_response.properties is not None + assert create_response.properties.description == SLI_DESCRIPTION + + # Step 2: Get SLI - verify it exists + get_response = self.client.slis.get( + service_group_name=SERVICE_GROUP_NAME, + sli_name=self.sli_name, + ) + assert get_response is not None + assert get_response.name in (self.sli_name, "Sanitized") + assert get_response.properties is not None + assert get_response.properties.description == SLI_DESCRIPTION + assert get_response.properties.sli_properties is not None + assert get_response.properties.sli_properties.window_uptime_criteria is not None + + # Step 3: Delete SLI + self.client.slis.delete( + service_group_name=SERVICE_GROUP_NAME, + sli_name=self.sli_name, + ) + + # Step 4: Get SLI - expect 404 + with pytest.raises(ResourceNotFoundError): + self.client.slis.get( + service_group_name=SERVICE_GROUP_NAME, + sli_name=self.sli_name, + ) + diff --git a/sdk/monitor/azure-mgmt-monitorslis/tsp-location.yaml b/sdk/monitor/azure-mgmt-monitorslis/tsp-location.yaml index b3ad897b7e77..2272356af846 100644 --- a/sdk/monitor/azure-mgmt-monitorslis/tsp-location.yaml +++ b/sdk/monitor/azure-mgmt-monitorslis/tsp-location.yaml @@ -1,4 +1,4 @@ directory: specification/monitoringservice/resource-manager/Microsoft.Monitor/Slis -commit: fa74a24ebfcd57429a95ef827b06eae0e639800a +commit: 8be8c75d9bb11ea95d8a7e251db74aa78b5cd76c repo: Azure/azure-rest-api-specs additionalDirectories: