From 12441c2f15e8c79eb3601fa912f7e62776b162cc Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Mon, 7 Jul 2025 21:37:03 +0000 Subject: [PATCH] [Monitor Ingestion] Add typespec generation files Signed-off-by: Paul Van Eck --- .../azure-monitor-ingestion/MANIFEST.in | 6 +- .../azure-monitor-ingestion/_metadata.json | 3 + .../apiview-properties.json | 4 + .../azure-monitor-ingestion/assets.json | 2 +- .../azure-monitor-ingestion/azure/__init__.py | 2 +- .../azure/monitor/__init__.py | 2 +- .../azure/monitor/ingestion/__init__.py | 5 +- .../azure/monitor/ingestion/_client.py | 18 +- .../azure/monitor/ingestion/_configuration.py | 19 +- .../monitor/ingestion/_operations/__init__.py | 2 +- .../ingestion/_operations/_operations.py | 59 +- .../monitor/ingestion/_operations/_patch.py | 10 +- .../monitor/ingestion/_utils/__init__.py | 6 + .../monitor/ingestion/_utils/model_base.py | 1232 +++++++++++++++++ .../serialization.py} | 178 +-- .../ingestion/{_vendor.py => _utils/utils.py} | 18 +- .../azure/monitor/ingestion/aio/__init__.py | 2 +- .../azure/monitor/ingestion/aio/_client.py | 18 +- .../monitor/ingestion/aio/_configuration.py | 19 +- .../ingestion/aio/_operations/__init__.py | 2 +- .../ingestion/aio/_operations/_operations.py | 54 +- .../ingestion/aio/_operations/_patch.py | 10 +- .../sample_upload_file_contents_async.py | 1 + .../sample_upload_pandas_dataframe_async.py | 1 + .../azure-monitor-ingestion/tests/conftest.py | 3 +- .../tests/test_logs_ingestion.py | 2 +- .../tests/test_logs_ingestion_async.py | 2 +- .../azure-monitor-ingestion/tsp-location.yaml | 4 + 28 files changed, 1437 insertions(+), 247 deletions(-) create mode 100644 sdk/monitor/azure-monitor-ingestion/_metadata.json create mode 100644 sdk/monitor/azure-monitor-ingestion/apiview-properties.json create mode 100644 sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/__init__.py create mode 100644 sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/model_base.py rename sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/{_serialization.py => _utils/serialization.py} (94%) rename sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/{_vendor.py => _utils/utils.py} (60%) create mode 100644 sdk/monitor/azure-monitor-ingestion/tsp-location.yaml diff --git a/sdk/monitor/azure-monitor-ingestion/MANIFEST.in b/sdk/monitor/azure-monitor-ingestion/MANIFEST.in index 516235bcc954..530ac02d030f 100644 --- a/sdk/monitor/azure-monitor-ingestion/MANIFEST.in +++ b/sdk/monitor/azure-monitor-ingestion/MANIFEST.in @@ -1,7 +1,7 @@ -recursive-include tests *.py -recursive-include samples *.py include *.md include LICENSE +include azure/monitor/ingestion/py.typed +recursive-include tests *.py +recursive-include samples *.py *.md include azure/__init__.py include azure/monitor/__init__.py -include azure/monitor/ingestion/py.typed diff --git a/sdk/monitor/azure-monitor-ingestion/_metadata.json b/sdk/monitor/azure-monitor-ingestion/_metadata.json new file mode 100644 index 000000000000..c17840f433c7 --- /dev/null +++ b/sdk/monitor/azure-monitor-ingestion/_metadata.json @@ -0,0 +1,3 @@ +{ + "apiVersion": "2023-01-01" +} \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-ingestion/apiview-properties.json b/sdk/monitor/azure-monitor-ingestion/apiview-properties.json new file mode 100644 index 000000000000..0ed55b910501 --- /dev/null +++ b/sdk/monitor/azure-monitor-ingestion/apiview-properties.json @@ -0,0 +1,4 @@ +{ + "CrossLanguagePackageId": "LogsIngestion", + "CrossLanguageDefinitionId": {} +} \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-ingestion/assets.json b/sdk/monitor/azure-monitor-ingestion/assets.json index 6d4a7997e009..feafedfcebfb 100644 --- a/sdk/monitor/azure-monitor-ingestion/assets.json +++ b/sdk/monitor/azure-monitor-ingestion/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/monitor/azure-monitor-ingestion", - "Tag": "python/monitor/azure-monitor-ingestion_12276a5674" + "Tag": "python/monitor/azure-monitor-ingestion_fd63ae0a28" } diff --git a/sdk/monitor/azure-monitor-ingestion/azure/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/__init__.py index 8db66d3d0f0f..d55ccad1f573 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/__init__.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/__init__.py @@ -1 +1 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/__init__.py index 8db66d3d0f0f..d55ccad1f573 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/__init__.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/__init__.py @@ -1 +1 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/__init__.py index 320e9412988f..7caa71199ec4 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/__init__.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/__init__.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- # pylint: disable=wrong-import-position @@ -13,6 +13,9 @@ from ._patch import * # pylint: disable=unused-wildcard-import from ._client import LogsIngestionClient # type: ignore +from ._version import VERSION + +__version__ = VERSION try: from ._patch import __all__ as _patch_all diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_client.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_client.py index e56331b0cc36..c72a33002214 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_client.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_client.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- @@ -16,28 +16,30 @@ from ._configuration import LogsIngestionClientConfiguration from ._operations import LogsIngestionClientOperationsMixin -from ._serialization import Deserializer, Serializer +from ._utils.serialization import Deserializer, Serializer if TYPE_CHECKING: from azure.core.credentials import TokenCredential class LogsIngestionClient(LogsIngestionClientOperationsMixin): - """Azure Monitor Data Collection Python Client. + """Azure Monitor data collection client. - :param endpoint: The Data Collection Endpoint for the Data Collection Rule, for example - https://dce-name.eastus-2.ingest.monitor.azure.com. Required. + :param endpoint: The Data Collection Endpoint for the Data Collection Rule. For example, + `https://dce-name.eastus-2.ingest.monitor.azure.com + `_. Required. :type endpoint: str - :param credential: Credential needed for the client to connect to Azure. Required. + :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: Api Version. Default value is "2023-01-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Default value is "2023-01-01". + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, credential: "TokenCredential", **kwargs: Any) -> None: _endpoint = "{endpoint}" self._config = LogsIngestionClientConfiguration(endpoint=endpoint, credential=credential, **kwargs) + _policies = kwargs.pop("policies", None) if _policies is None: _policies = [ diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_configuration.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_configuration.py index 02a9843af91a..1627cc1d3384 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_configuration.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_configuration.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- @@ -10,11 +10,11 @@ from azure.core.pipeline import policies +from ._version import VERSION + if TYPE_CHECKING: from azure.core.credentials import TokenCredential -VERSION = "unknown" - class LogsIngestionClientConfiguration: # pylint: disable=too-many-instance-attributes """Configuration for LogsIngestionClient. @@ -22,13 +22,14 @@ class LogsIngestionClientConfiguration: # pylint: disable=too-many-instance-att Note that all parameters used to create this instance are saved as instance attributes. - :param endpoint: The Data Collection Endpoint for the Data Collection Rule, for example - https://dce-name.eastus-2.ingest.monitor.azure.com. Required. + :param endpoint: The Data Collection Endpoint for the Data Collection Rule. For example, + `https://dce-name.eastus-2.ingest.monitor.azure.com + `_. Required. :type endpoint: str - :param credential: Credential needed for the client to connect to Azure. Required. + :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: Api Version. Default value is "2023-01-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Default value is "2023-01-01". + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ @@ -43,7 +44,7 @@ def __init__(self, endpoint: str, credential: "TokenCredential", **kwargs: Any) self.endpoint = endpoint self.credential = credential self.api_version = api_version - self.credential_scopes = kwargs.pop("credential_scopes", ["https://monitor.azure.com//.default"]) + self.credential_scopes = kwargs.pop("credential_scopes", ["https://monitor.azure.com/.default"]) kwargs.setdefault("sdk_moniker", "monitor-ingestion/{}".format(VERSION)) self.polling_interval = kwargs.get("polling_interval", 30) self._configure(**kwargs) diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/__init__.py index 4bf65f393a39..34ad2ded0df9 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/__init__.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/__init__.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- # pylint: disable=wrong-import-position diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_operations.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_operations.py index 2eff843c47dc..100e69af92e5 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_operations.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_operations.py @@ -2,13 +2,15 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from collections.abc import MutableMapping from io import IOBase -import sys +import json from typing import Any, Callable, Dict, IO, List, Optional, TypeVar, Union, overload +from azure.core import PipelineClient from azure.core.exceptions import ( ClientAuthenticationError, HttpResponseError, @@ -22,14 +24,11 @@ from azure.core.tracing.decorator import distributed_trace from azure.core.utils import case_insensitive_dict -from .._serialization import Serializer -from .._vendor import LogsIngestionClientMixinABC +from .._configuration import LogsIngestionClientConfiguration +from .._utils.model_base import SdkJSONEncoder +from .._utils.serialization import Serializer +from .._utils.utils import ClientMixinABC -if sys.version_info >= (3, 9): - from collections.abc import MutableMapping -else: - from typing import MutableMapping # type: ignore -JSON = MutableMapping[str, Any] # pylint: disable=unsubscriptable-object T = TypeVar("T") ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] @@ -38,7 +37,7 @@ def build_logs_ingestion_upload_request( - rule_id: str, stream: str, *, content_encoding: Optional[str] = None, **kwargs: Any + rule_id: str, stream_name: str, *, content_encoding: Optional[str] = None, **kwargs: Any ) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) @@ -51,7 +50,7 @@ def build_logs_ingestion_upload_request( _url = "/dataCollectionRules/{ruleId}/streams/{stream}" path_format_arguments = { "ruleId": _SERIALIZER.url("rule_id", rule_id, "str"), - "stream": _SERIALIZER.url("stream", stream, "str"), + "stream": _SERIALIZER.url("stream_name", stream_name, "str"), } _url: str = _url.format(**path_format_arguments) # type: ignore @@ -69,14 +68,16 @@ def build_logs_ingestion_upload_request( return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) -class LogsIngestionClientOperationsMixin(LogsIngestionClientMixinABC): +class LogsIngestionClientOperationsMixin( + ClientMixinABC[PipelineClient[HttpRequest, HttpResponse], LogsIngestionClientConfiguration] +): @overload def _upload( self, rule_id: str, - stream: str, - body: List[JSON], + stream_name: str, + body: List[Dict[str, Any]], *, content_encoding: Optional[str] = None, content_type: str = "application/json", @@ -86,7 +87,7 @@ def _upload( def _upload( self, rule_id: str, - stream: str, + stream_name: str, body: IO[bytes], *, content_encoding: Optional[str] = None, @@ -98,24 +99,26 @@ def _upload( def _upload( # pylint: disable=inconsistent-return-statements self, rule_id: str, - stream: str, - body: Union[List[JSON], IO[bytes]], + stream_name: str, + body: Union[List[Dict[str, Any]], IO[bytes]], *, content_encoding: Optional[str] = None, **kwargs: Any ) -> None: """Ingestion API used to directly ingest data using Data Collection Rules. - See error response code and error response message for more detail. + Ingestion API used to directly ingest data using Data Collection Rules. - :param rule_id: The immutable Id of the Data Collection Rule resource. Required. + :param rule_id: The immutable ID of the Data Collection Rule resource. Required. :type rule_id: str - :param stream: The streamDeclaration name as defined in the Data Collection Rule. Required. - :type stream: str - :param body: An array of objects matching the schema defined by the provided stream. Is either - a [JSON] type or a IO[bytes] type. Required. - :type body: list[JSON] or IO[bytes] - :keyword content_encoding: gzip. Default value is None. + :param stream_name: The streamDeclaration name as defined in the Data Collection Rule. + Required. + :type stream_name: str + :param body: The array of objects matching the schema defined by the provided stream. Is either + a [{str: Any}] type or a IO[bytes] type. Required. + :type body: list[dict[str, any]] or IO[bytes] + :keyword content_encoding: The content encoding of the request body which is always 'gzip'. + Default value is None. :paramtype content_encoding: str :return: None :rtype: None @@ -136,20 +139,18 @@ def _upload( # pylint: disable=inconsistent-return-statements cls: ClsType[None] = kwargs.pop("cls", None) content_type = content_type or "application/json" - _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _json = body + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore _request = build_logs_ingestion_upload_request( rule_id=rule_id, - stream=stream, + stream_name=stream_name, content_encoding=content_encoding, content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_patch.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_patch.py index ac2553c47196..a997f12d70a5 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_patch.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_operations/_patch.py @@ -65,7 +65,13 @@ def upload( content_encoding = "gzip" logs.seek(0) - super()._upload(rule_id, stream=stream_name, body=logs, content_encoding=content_encoding, **kwargs) + super()._upload( + rule_id, + stream_name=stream_name, + body=cast(IO[bytes], logs), + content_encoding=content_encoding, + **kwargs + ) return if not isinstance(logs, Sequence) or isinstance(logs, str): @@ -76,7 +82,7 @@ def upload( for gzip_data, log_chunk in _create_gzip_requests(cast(List[JSON], logs)): try: super()._upload( # type: ignore - rule_id, stream=stream_name, body=gzip_data, content_encoding="gzip", **kwargs # type: ignore + rule_id, stream_name=stream_name, body=gzip_data, content_encoding="gzip", **kwargs # type: ignore ) except Exception as err: # pylint: disable=broad-except if on_error: diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/__init__.py new file mode 100644 index 000000000000..8026245c2abc --- /dev/null +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------- +# 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. +# -------------------------------------------------------------------------- diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/model_base.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/model_base.py new file mode 100644 index 000000000000..49d5c7259389 --- /dev/null +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/model_base.py @@ -0,0 +1,1232 @@ +# pylint: disable=too-many-lines +# 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. +# -------------------------------------------------------------------------- +# pylint: disable=protected-access, broad-except + +import copy +import calendar +import decimal +import functools +import sys +import logging +import base64 +import re +import typing +import enum +import email.utils +from datetime import datetime, date, time, timedelta, timezone +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 + +_LOGGER = logging.getLogger(__name__) + +__all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] + +TZ_UTC = timezone.utc +_T = typing.TypeVar("_T") + + +def _timedelta_as_isostr(td: timedelta) -> str: + """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S' + + Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython + + :param timedelta td: The timedelta to convert + :rtype: str + :return: ISO8601 version of this timedelta + """ + + # Split seconds to larger units + seconds = td.total_seconds() + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + + days, hours, minutes = list(map(int, (days, hours, minutes))) + seconds = round(seconds, 6) + + # Build date + date_str = "" + if days: + date_str = "%sD" % days + + if hours or minutes or seconds: + # Build time + time_str = "T" + + # Hours + bigger_exists = date_str or hours + if bigger_exists: + time_str += "{:02}H".format(hours) + + # Minutes + bigger_exists = bigger_exists or minutes + if bigger_exists: + time_str += "{:02}M".format(minutes) + + # Seconds + try: + if seconds.is_integer(): + seconds_string = "{:02}".format(int(seconds)) + else: + # 9 chars long w/ leading 0, 6 digits after decimal + seconds_string = "%09.6f" % seconds + # Remove trailing zeros + seconds_string = seconds_string.rstrip("0") + except AttributeError: # int.is_integer() raises + seconds_string = "{:02}".format(seconds) + + time_str += "{}S".format(seconds_string) + else: + time_str = "" + + return "P" + date_str + time_str + + +def _serialize_bytes(o, format: typing.Optional[str] = None) -> str: + encoded = base64.b64encode(o).decode() + if format == "base64url": + return encoded.strip("=").replace("+", "-").replace("/", "_") + return encoded + + +def _serialize_datetime(o, format: typing.Optional[str] = None): + if hasattr(o, "year") and hasattr(o, "hour"): + if format == "rfc7231": + return email.utils.format_datetime(o, usegmt=True) + if format == "unix-timestamp": + return int(calendar.timegm(o.utctimetuple())) + + # astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set) + if not o.tzinfo: + iso_formatted = o.replace(tzinfo=TZ_UTC).isoformat() + else: + iso_formatted = o.astimezone(TZ_UTC).isoformat() + # Replace the trailing "+00:00" UTC offset with "Z" (RFC 3339: https://www.ietf.org/rfc/rfc3339.txt) + return iso_formatted.replace("+00:00", "Z") + # Next try datetime.date or datetime.time + return o.isoformat() + + +def _is_readonly(p): + try: + return p._visibility == ["read"] + except AttributeError: + return False + + +class SdkJSONEncoder(JSONEncoder): + """A JSON encoder that's capable of serializing datetime objects and bytes.""" + + def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs): + super().__init__(*args, **kwargs) + self.exclude_readonly = exclude_readonly + self.format = format + + def default(self, o): # pylint: disable=too-many-return-statements + if _is_model(o): + if self.exclude_readonly: + readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] + return {k: v for k, v in o.items() if k not in readonly_props} + return dict(o.items()) + try: + return super(SdkJSONEncoder, self).default(o) + except TypeError: + if isinstance(o, _Null): + return None + if isinstance(o, decimal.Decimal): + return float(o) + if isinstance(o, (bytes, bytearray)): + return _serialize_bytes(o, self.format) + try: + # First try datetime.datetime + return _serialize_datetime(o, self.format) + except AttributeError: + pass + # Last, try datetime.timedelta + try: + return _timedelta_as_isostr(o) + except AttributeError: + # This will be raised when it hits value.total_seconds in the method above + pass + return super(SdkJSONEncoder, self).default(o) + + +_VALID_DATE = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" + r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") +_VALID_RFC7231 = re.compile( + r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s" + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT" +) + + +def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: + """Deserialize ISO-8601 formatted string into Datetime object. + + :param str attr: response string to be deserialized. + :rtype: ~datetime.datetime + :returns: The datetime object from that input + """ + if isinstance(attr, datetime): + # i'm already deserialized + return attr + attr = attr.upper() + match = _VALID_DATE.match(attr) + if not match: + raise ValueError("Invalid datetime string: " + attr) + + check_decimal = attr.split(".") + if len(check_decimal) > 1: + decimal_str = "" + for digit in check_decimal[1]: + if digit.isdigit(): + decimal_str += digit + else: + break + if len(decimal_str) > 6: + attr = attr.replace(decimal_str, decimal_str[0:6]) + + date_obj = isodate.parse_datetime(attr) + 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 + + +def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime: + """Deserialize RFC7231 formatted string into Datetime object. + + :param str attr: response string to be deserialized. + :rtype: ~datetime.datetime + :returns: The datetime object from that input + """ + if isinstance(attr, datetime): + # i'm already deserialized + return attr + match = _VALID_RFC7231.match(attr) + if not match: + raise ValueError("Invalid datetime string: " + attr) + + return email.utils.parsedate_to_datetime(attr) + + +def _deserialize_datetime_unix_timestamp(attr: typing.Union[float, datetime]) -> datetime: + """Deserialize unix timestamp into Datetime object. + + :param str attr: response string to be deserialized. + :rtype: ~datetime.datetime + :returns: The datetime object from that input + """ + if isinstance(attr, datetime): + # i'm already deserialized + return attr + return datetime.fromtimestamp(attr, TZ_UTC) + + +def _deserialize_date(attr: typing.Union[str, date]) -> date: + """Deserialize ISO-8601 formatted string into Date object. + :param str attr: response string to be deserialized. + :rtype: date + :returns: The date object from that input + """ + # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. + if isinstance(attr, date): + return attr + return isodate.parse_date(attr, defaultmonth=None, defaultday=None) # type: ignore + + +def _deserialize_time(attr: typing.Union[str, time]) -> time: + """Deserialize ISO-8601 formatted string into time object. + + :param str attr: response string to be deserialized. + :rtype: datetime.time + :returns: The time object from that input + """ + if isinstance(attr, time): + return attr + return isodate.parse_time(attr) + + +def _deserialize_bytes(attr): + if isinstance(attr, (bytes, bytearray)): + return attr + return bytes(base64.b64decode(attr)) + + +def _deserialize_bytes_base64(attr): + if isinstance(attr, (bytes, bytearray)): + return attr + padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore + attr = attr + padding # type: ignore + encoded = attr.replace("-", "+").replace("_", "/") + return bytes(base64.b64decode(encoded)) + + +def _deserialize_duration(attr): + if isinstance(attr, timedelta): + return attr + return isodate.parse_duration(attr) + + +def _deserialize_decimal(attr): + if isinstance(attr, decimal.Decimal): + return attr + return decimal.Decimal(str(attr)) + + +def _deserialize_int_as_str(attr): + if isinstance(attr, int): + return attr + return int(attr) + + +_DESERIALIZE_MAPPING = { + datetime: _deserialize_datetime, + date: _deserialize_date, + time: _deserialize_time, + bytes: _deserialize_bytes, + bytearray: _deserialize_bytes, + timedelta: _deserialize_duration, + typing.Any: lambda x: x, + decimal.Decimal: _deserialize_decimal, +} + +_DESERIALIZE_MAPPING_WITHFORMAT = { + "rfc3339": _deserialize_datetime, + "rfc7231": _deserialize_datetime_rfc7231, + "unix-timestamp": _deserialize_datetime_unix_timestamp, + "base64": _deserialize_bytes, + "base64url": _deserialize_bytes_base64, +} + + +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 rf and rf._format: + return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) + return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore + + +def _get_type_alias_type(module_name: str, alias_name: str): + types = { + k: v + for k, v in sys.modules[module_name].__dict__.items() + if isinstance(v, typing._GenericAlias) # type: ignore + } + if alias_name not in types: + return alias_name + return types[alias_name] + + +def _get_model(module_name: str, model_name: str): + models = {k: v for k, v in sys.modules[module_name].__dict__.items() if isinstance(v, type)} + module_end = module_name.rsplit(".", 1)[0] + models.update({k: v for k, v in sys.modules[module_end].__dict__.items() if isinstance(v, type)}) + if isinstance(model_name, str): + model_name = model_name.split(".")[-1] + if model_name not in models: + return model_name + return models[model_name] + + +_UNSET = object() + + +class _MyMutableMapping(MutableMapping[str, typing.Any]): + def __init__(self, data: typing.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: + return self._data.__getitem__(key) + + def __setitem__(self, key: str, value: typing.Any) -> None: + self._data.__setitem__(key, value) + + def __delitem__(self, key: str) -> None: + self._data.__delitem__(key) + + def __iter__(self) -> typing.Iterator[typing.Any]: + return self._data.__iter__() + + def __len__(self) -> int: + return self._data.__len__() + + def __ne__(self, other: typing.Any) -> bool: + return not self.__eq__(other) + + def keys(self) -> typing.KeysView[str]: + """ + :returns: a set-like object providing a view on D's keys + :rtype: ~typing.KeysView + """ + return self._data.keys() + + def values(self) -> typing.ValuesView[typing.Any]: + """ + :returns: an object providing a view on D's values + :rtype: ~typing.ValuesView + """ + return self._data.values() + + def items(self) -> typing.ItemsView[str, typing.Any]: + """ + :returns: set-like object providing a view on D's items + :rtype: ~typing.ItemsView + """ + return self._data.items() + + def get(self, key: str, default: typing.Any = None) -> typing.Any: + """ + Get the value for key if key is in the dictionary, else default. + :param str key: The key to look up. + :param any default: The value to return if key is not in the dictionary. Defaults to None + :returns: D[k] if k in D, else d. + :rtype: any + """ + try: + return self[key] + except KeyError: + return default + + @typing.overload + def pop(self, key: str) -> typing.Any: ... # pylint: disable=arguments-differ + + @typing.overload + def pop(self, key: str, default: _T) -> _T: ... # pylint: disable=signature-differs + + @typing.overload + def pop(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs + + def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any: + """ + Removes specified key and return the corresponding value. + :param str key: The key to pop. + :param any default: The value to return if key is not in the dictionary + :returns: The value corresponding to the key. + :rtype: any + :raises KeyError: If key is not found and default is not given. + """ + if default is _UNSET: + return self._data.pop(key) + return self._data.pop(key, default) + + def popitem(self) -> typing.Tuple[str, typing.Any]: + """ + Removes and returns some (key, value) pair + :returns: The (key, value) pair. + :rtype: tuple + :raises KeyError: if D is empty. + """ + return self._data.popitem() + + def clear(self) -> None: + """ + Remove all items from D. + """ + self._data.clear() + + def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: # pylint: disable=arguments-differ + """ + Updates D from mapping/iterable E and F. + :param any args: Either a mapping object or an iterable of key-value pairs. + """ + self._data.update(*args, **kwargs) + + @typing.overload + def setdefault(self, key: str, default: None = None) -> None: ... + + @typing.overload + def setdefault(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs + + def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any: + """ + Same as calling D.get(k, d), and setting D[k]=d if k not found + :param str key: The key to look up. + :param any default: The value to set if key is not in the dictionary + :returns: D[k] if k in D, else d. + :rtype: any + """ + if default is _UNSET: + return self._data.setdefault(key) + return self._data.setdefault(key, default) + + def __eq__(self, other: typing.Any) -> bool: + try: + other_model = self.__class__(other) + except Exception: + return False + return self._data == other_model._data + + def __repr__(self) -> str: + return str(self._data) + + +def _is_model(obj: typing.Any) -> bool: + return getattr(obj, "_is_model", False) + + +def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements + if isinstance(o, list): + return [_serialize(x, format) for x in o] + if isinstance(o, dict): + return {k: _serialize(v, format) for k, v in o.items()} + if isinstance(o, set): + return {_serialize(x, format) for x in o} + if isinstance(o, tuple): + return tuple(_serialize(x, format) for x in o) + if isinstance(o, (bytes, bytearray)): + return _serialize_bytes(o, format) + if isinstance(o, decimal.Decimal): + return float(o) + if isinstance(o, enum.Enum): + return o.value + if isinstance(o, int): + if format == "str": + return str(o) + return o + try: + # First try datetime.datetime + return _serialize_datetime(o, format) + except AttributeError: + pass + # Last, try datetime.timedelta + try: + return _timedelta_as_isostr(o) + except AttributeError: + # This will be raised when it hits value.total_seconds in the method above + pass + return o + + +def _get_rest_field( + attr_to_rest_field: typing.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: + return None + + +def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any: + if not rf: + return _serialize(value, None) + if rf._is_multipart_file_input: + return value + if rf._is_model: + return _deserialize(rf._type, value) + if isinstance(value, ET.Element): + value = _deserialize(rf._type, value) + return _serialize(value, rf._format) + + +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() + + 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 + } + if args: # pylint: disable=too-many-nested-blocks + 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) + 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()} + ) + else: + non_attr_kwargs = [k for k in kwargs if k not in self._attr_to_rest_field] + if non_attr_kwargs: + # actual type errors only throw the first wrong keyword arg they see, so following that. + raise TypeError(f"{class_name}.__init__() got an unexpected keyword argument '{non_attr_kwargs[0]}'") + dict_to_pass.update( + { + self._attr_to_rest_field[k]._rest_name: _create_value(self._attr_to_rest_field[k], v) + for k, v in kwargs.items() + if v is not None + } + ) + super().__init__(dict_to_pass) + + def copy(self) -> "Model": + return Model(self.__dict__) + + def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: + if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated: + # 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 + k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type") + } + annotations = { + k: v + for mro_class in mros + if hasattr(mro_class, "__annotations__") + for k, v in mro_class.__annotations__.items() + } + for attr, rf in attr_to_rest_field.items(): + rf._module = cls.__module__ + if not rf._type: + 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._calculated.add(f"{cls.__module__}.{cls.__qualname__}") + + return super().__new__(cls) + + def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None: + for base in cls.__bases__: + if hasattr(base, "__mapping__"): + base.__mapping__[discriminator or cls.__name__] = cls # type: ignore + + @classmethod + def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: + for v in cls.__dict__.values(): + if isinstance(v, _RestField) and v._is_discriminator and v._rest_name not in exist_discriminators: + return v + return None + + @classmethod + def _deserialize(cls, data, exist_discriminators): + if not hasattr(cls, "__mapping__"): + return cls(data) + discriminator = cls._get_discriminator(exist_discriminators) + if discriminator is None: + return cls(data) + exist_discriminators.append(discriminator._rest_name) + if isinstance(data, ET.Element): + 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)) + if xml_ns: + xml_name = "{" + xml_ns + "}" + xml_name + + if data.get(xml_name) is not None: + discriminator_value = data.get(xml_name) + else: + discriminator_value = data.find(xml_name).text # pyright: ignore + else: + discriminator_value = data.get(discriminator._rest_name) + 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]: + """Return a dict that can be turned into json using json.dump. + + :keyword bool exclude_readonly: Whether to remove the readonly properties. + :returns: A dict JSON compatible object + :rtype: dict + """ + + result = {} + readonly_props = [] + if exclude_readonly: + readonly_props = [p._rest_name for p in self._attr_to_rest_field.values() if _is_readonly(p)] + for k, v in self.items(): + if exclude_readonly and k in readonly_props: # pyright: ignore + continue + is_multipart_file_input = False + try: + is_multipart_file_input = next( + rf for rf in self._attr_to_rest_field.values() if rf._rest_name == k + )._is_multipart_file_input + except StopIteration: + pass + result[k] = v if is_multipart_file_input else Model._as_dict_value(v, exclude_readonly=exclude_readonly) + return result + + @staticmethod + def _as_dict_value(v: typing.Any, exclude_readonly: bool = False) -> typing.Any: + if v is None or isinstance(v, _Null): + return None + if isinstance(v, (list, tuple, set)): + return type(v)(Model._as_dict_value(x, exclude_readonly=exclude_readonly) for x in v) + if isinstance(v, dict): + return {dk: Model._as_dict_value(dv, exclude_readonly=exclude_readonly) for dk, dv in v.items()} + return v.as_dict(exclude_readonly=exclude_readonly) if hasattr(v, "as_dict") else v + + +def _deserialize_model(model_deserializer: typing.Optional[typing.Callable], obj): + if _is_model(obj): + return obj + return _deserialize(model_deserializer, obj) + + +def _deserialize_with_optional(if_obj_deserializer: typing.Optional[typing.Callable], obj): + if obj is None: + return obj + return _deserialize_with_callable(if_obj_deserializer, obj) + + +def _deserialize_with_union(deserializers, obj): + for deserializer in deserializers: + try: + return _deserialize(deserializer, obj) + except DeserializationError: + pass + raise DeserializationError() + + +def _deserialize_dict( + value_deserializer: typing.Optional[typing.Callable], + module: typing.Optional[str], + obj: typing.Dict[typing.Any, typing.Any], +): + if obj is None: + return obj + if isinstance(obj, ET.Element): + obj = {child.tag: child for child in obj} + return {k: _deserialize(value_deserializer, v, module) for k, v in obj.items()} + + +def _deserialize_multiple_sequence( + entry_deserializers: typing.List[typing.Optional[typing.Callable]], + module: typing.Optional[str], + obj, +): + if obj is None: + return obj + return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers)) + + +def _deserialize_sequence( + deserializer: typing.Optional[typing.Callable], + module: typing.Optional[str], + obj, +): + if obj is None: + return obj + if isinstance(obj, ET.Element): + obj = list(obj) + return type(obj)(_deserialize(deserializer, entry, module) for entry in obj) + + +def _sorted_annotations(types: typing.List[typing.Any]) -> typing.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 + annotation: typing.Any, + module: typing.Optional[str], + rf: typing.Optional["_RestField"] = None, +) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: + if not annotation: + return None + + # is it a type alias? + if isinstance(annotation, str): + if module is not None: + annotation = _get_type_alias_type(module, annotation) + + # is it a forward ref / in quotes? + if isinstance(annotation, (str, typing.ForwardRef)): + try: + model_name = annotation.__forward_arg__ # type: ignore + except AttributeError: + model_name = annotation + if module is not None: + annotation = _get_model(module, model_name) # type: ignore + + try: + if module and _is_model(annotation): + if rf: + rf._is_model = True + + return functools.partial(_deserialize_model, annotation) # pyright: ignore + except Exception: + pass + + # is it a literal? + try: + if annotation.__origin__ is typing.Literal: # pyright: ignore + return None + except AttributeError: + pass + + # is it optional? + try: + if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore + 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 + ) + + 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 + return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) + except AttributeError: + pass + + # is it union? + if getattr(annotation, "__origin__", None) is typing.Union: + # initial ordering is we make `string` the last deserialization option, because it is often them most generic + deserializers = [ + _get_deserialize_callable_from_annotation(arg, module, rf) + for arg in _sorted_annotations(annotation.__args__) # pyright: ignore + ] + + return functools.partial(_deserialize_with_union, deserializers) + + try: + if annotation._name == "Dict": # pyright: ignore + value_deserializer = _get_deserialize_callable_from_annotation( + annotation.__args__[1], module, rf # pyright: ignore + ) + + return functools.partial( + _deserialize_dict, + value_deserializer, + module, + ) + except (AttributeError, IndexError): + pass + try: + if annotation._name in ["List", "Set", "Tuple", "Sequence"]: # pyright: ignore + if len(annotation.__args__) > 1: # pyright: ignore + entry_deserializers = [ + _get_deserialize_callable_from_annotation(dt, module, rf) + for dt in annotation.__args__ # pyright: ignore + ] + return functools.partial(_deserialize_multiple_sequence, entry_deserializers, module) + deserializer = _get_deserialize_callable_from_annotation( + annotation.__args__[0], module, rf # pyright: ignore + ) + + return functools.partial(_deserialize_sequence, deserializer, module) + except (TypeError, IndexError, AttributeError, SyntaxError): + pass + + def _deserialize_default( + deserializer, + obj, + ): + if obj is None: + return obj + try: + return _deserialize_with_callable(deserializer, obj) + except Exception: + pass + return obj + + if get_deserializer(annotation, rf): + return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) + + return functools.partial(_deserialize_default, annotation) + + +def _deserialize_with_callable( + deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]], + value: typing.Any, +): # pylint: disable=too-many-return-statements + try: + if value is None or isinstance(value, _Null): + return None + if isinstance(value, ET.Element): + if deserializer is str: + return value.text or "" + if deserializer is int: + return int(value.text) if value.text else None + if deserializer is float: + return float(value.text) if value.text else None + if deserializer is bool: + return value.text == "true" 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) + except ValueError: + # for unknown value, return raw value + return value + if isinstance(deserializer, type) and issubclass(deserializer, Model): + return deserializer._deserialize(value, []) + return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value) + except Exception as e: + raise DeserializationError() from e + + +def _deserialize( + deserializer: typing.Any, + value: typing.Any, + module: typing.Optional[str] = None, + rf: typing.Optional["_RestField"] = None, + format: typing.Optional[str] = None, +) -> typing.Any: + if isinstance(value, PipelineResponse): + value = value.http_response.json() + if rf is None and format: + rf = _RestField(format=format) + if not isinstance(deserializer, functools.partial): + deserializer = _get_deserialize_callable_from_annotation(deserializer, module, rf) + return _deserialize_with_callable(deserializer, value) + + +def _failsafe_deserialize( + deserializer: typing.Any, + value: typing.Any, + 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: + _LOGGER.warning( + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True + ) + return None + + +def _failsafe_deserialize_xml( + deserializer: typing.Any, + value: typing.Any, +) -> typing.Any: + try: + return _deserialize_xml(deserializer, value) + except DeserializationError: + _LOGGER.warning( + "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True + ) + return None + + +class _RestField: + def __init__( + self, + *, + 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, + default: typing.Any = _UNSET, + format: typing.Optional[str] = None, + is_multipart_file_input: bool = False, + xml: typing.Optional[typing.Dict[str, typing.Any]] = None, + ): + self._type = type + self._rest_name_input = name + self._module: typing.Optional[str] = None + self._is_discriminator = is_discriminator + self._visibility = visibility + self._is_model = False + self._default = default + self._format = format + self._is_multipart_file_input = is_multipart_file_input + self._xml = xml if xml is not None else {} + + @property + def _class_type(self) -> typing.Any: + return getattr(self._type, "args", [None])[0] + + @property + def _rest_name(self) -> str: + if self._rest_name_input is None: + raise ValueError("Rest name was never set") + return self._rest_name_input + + 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) + if item is None: + return item + if self._is_model: + return item + return _deserialize(self._type, _serialize(item, self._format), rf=self) + + def __set__(self, obj: Model, value) -> None: + if value is None: + # we want to wipe out entries if users set attr to None + try: + obj.__delitem__(self._rest_name) + except KeyError: + pass + return + if self._is_model: + if not _is_model(value): + value = _deserialize(self._type, value) + obj.__setitem__(self._rest_name, value) + return + obj.__setitem__(self._rest_name, _serialize(value, self._format)) + + def _get_deserialize_callable_from_annotation( + self, annotation: typing.Any + ) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: + return _get_deserialize_callable_from_annotation(annotation, self._module, self) + + +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, + default: typing.Any = _UNSET, + format: typing.Optional[str] = None, + is_multipart_file_input: bool = False, + xml: typing.Optional[typing.Dict[str, typing.Any]] = None, +) -> typing.Any: + return _RestField( + name=name, + type=type, + visibility=visibility, + default=default, + format=format, + is_multipart_file_input=is_multipart_file_input, + xml=xml, + ) + + +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, +) -> typing.Any: + return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml) + + +def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: + """Serialize a model to XML. + + :param Model model: The model to serialize. + :param bool exclude_readonly: Whether to exclude readonly properties. + :returns: The XML representation of the model. + :rtype: str + """ + return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore + + +def _get_element( + o: typing.Any, + exclude_readonly: bool = False, + parent_meta: typing.Optional[typing.Dict[str, typing.Any]] = None, + wrapped_element: typing.Optional[ET.Element] = None, +) -> typing.Union[ET.Element, typing.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: + wrapped_element = _create_xml_element( + model_meta.get("name", o.__class__.__name__), + model_meta.get("prefix"), + model_meta.get("ns"), + ) + + readonly_props = [] + if exclude_readonly: + readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] + + for k, v in o.items(): + # do not serialize readonly properties + if exclude_readonly and k in readonly_props: + continue + + prop_rest_field = _get_rest_field(o._attr_to_rest_field, k) + if prop_rest_field: + prop_meta = getattr(prop_rest_field, "_xml").copy() + # use the wire name as xml name if no specific name is set + if prop_meta.get("name") is None: + prop_meta["name"] = k + else: + # 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 + 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") + + if prop_meta.get("unwrapped", False): + # unwrapped could only set on array + wrapped_element.extend(_get_element(v, exclude_readonly, prop_meta)) + elif prop_meta.get("text", False): + # 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)) + else: + # other wrapped prop element + wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) + return wrapped_element + if isinstance(o, list): + return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore + if isinstance(o, dict): + result = [] + for k, v in o.items(): + result.append( + _get_wrapped_element( + v, + exclude_readonly, + { + "name": k, + "ns": parent_meta.get("ns") if parent_meta else None, + "prefix": parent_meta.get("prefix") if parent_meta else None, + }, + ) + ) + return result + + # primitive case need to create element based on parent_meta + if 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")), + }, + ) + + raise ValueError("Could not serialize value into xml: " + o) + + +def _get_wrapped_element( + v: typing.Any, + exclude_readonly: bool, + meta: typing.Optional[typing.Dict[str, typing.Any]], +) -> ET.Element: + 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 + ) + if isinstance(v, (dict, list)): + wrapped_element.extend(_get_element(v, exclude_readonly, meta)) + elif _is_model(v): + _get_element(v, exclude_readonly, meta, wrapped_element) + else: + wrapped_element.text = _get_primitive_type_value(v) + return wrapped_element + + +def _get_primitive_type_value(v) -> str: + if v is True: + return "true" + if v is False: + return "false" + if isinstance(v, _Null): + return "" + return str(v) + + +def _create_xml_element(tag, prefix=None, ns=None): + if prefix and ns: + ET.register_namespace(prefix, ns) + if ns: + return ET.Element("{" + ns + "}" + tag) + return ET.Element(tag) + + +def _deserialize_xml( + deserializer: typing.Any, + value: str, +) -> typing.Any: + element = ET.fromstring(value) # nosec + return _deserialize(deserializer, element) + + +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] = {} + for child in e: + if dict_result.get(child.tag) is not None: + if isinstance(dict_result[child.tag], list): + dict_result[child.tag].append(_convert_element(child)) + else: + dict_result[child.tag] = [dict_result[child.tag], _convert_element(child)] + else: + dict_result[child.tag] = _convert_element(child) + dict_result.update(e.attrib) + return dict_result + # array case + if len(e) > 0: + array_result: typing.List[typing.Any] = [] + for child in e: + array_result.append(_convert_element(child)) + return array_result + # primitive case + return e.text diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_serialization.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/serialization.py similarity index 94% rename from sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_serialization.py rename to sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/serialization.py index b24ab2885450..eb86ea23c965 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_serialization.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/serialization.py @@ -1,28 +1,10 @@ -# 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. -# -# The MIT License (MIT) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the ""Software""), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. -# +# 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. # -------------------------------------------------------------------------- # pyright: reportUnnecessaryTypeIgnoreComment=false @@ -48,9 +30,7 @@ IO, Mapping, Callable, - TypeVar, MutableMapping, - Type, List, ) @@ -61,13 +41,13 @@ 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 _BOM = codecs.BOM_UTF8.decode(encoding="utf-8") -ModelType = TypeVar("ModelType", bound="Model") JSON = MutableMapping[str, Any] @@ -185,73 +165,7 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], except NameError: _long_type = int - -class UTC(datetime.tzinfo): - """Time Zone info for handling UTC""" - - def utcoffset(self, dt): - """UTF offset for UTC is 0. - - :param datetime.datetime dt: The datetime - :returns: The offset - :rtype: datetime.timedelta - """ - return datetime.timedelta(0) - - def tzname(self, dt): - """Timestamp representation. - - :param datetime.datetime dt: The datetime - :returns: The timestamp representation - :rtype: str - """ - return "Z" - - def dst(self, dt): - """No daylight saving for UTC. - - :param datetime.datetime dt: The datetime - :returns: The daylight saving time - :rtype: datetime.timedelta - """ - return datetime.timedelta(hours=1) - - -try: - from datetime import timezone as _FixedOffset # type: ignore -except ImportError: # Python 2.7 - - class _FixedOffset(datetime.tzinfo): # type: ignore - """Fixed offset in minutes east from UTC. - Copy/pasted from Python doc - :param datetime.timedelta offset: offset in timedelta format - """ - - def __init__(self, offset) -> None: - self.__offset = offset - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return str(self.__offset.total_seconds() / 3600) - - def __repr__(self): - return "".format(self.tzname(None)) - - def dst(self, dt): - return datetime.timedelta(0) - - def __getinitargs__(self): - return (self.__offset,) - - -try: - from datetime import timezone - - TZ_UTC = timezone.utc -except ImportError: - TZ_UTC = UTC() # type: ignore +TZ_UTC = datetime.timezone.utc _FLATTEN = re.compile(r"(? ModelType: + def deserialize(cls, data: Any, content_type: Optional[str] = None) -> Self: """Parse a str using the RestAPI syntax and return a model. :param str data: A str using RestAPI structure. JSON by default. :param str content_type: JSON by default, set application/xml if XML. :returns: An instance of this model - :raises: DeserializationError if something went wrong - :rtype: ModelType + :raises DeserializationError: if something went wrong + :rtype: Self """ deserializer = Deserializer(cls._infer_class_models()) return deserializer(cls.__name__, data, content_type=content_type) # type: ignore @classmethod def from_dict( - cls: Type[ModelType], + cls, data: Any, key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, content_type: Optional[str] = None, - ) -> ModelType: + ) -> Self: """Parse a dict using given key extractor return a model. By default consider key @@ -479,8 +393,8 @@ def from_dict( :param function key_extractors: A key extractor function. :param str content_type: JSON by default, set application/xml if XML. :returns: An instance of this model - :raises: DeserializationError if something went wrong - :rtype: ModelType + :raises DeserializationError: if something went wrong + :rtype: Self """ deserializer = Deserializer(cls._infer_class_models()) deserializer.key_extractors = ( # type: ignore @@ -626,7 +540,7 @@ def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, to :param object target_obj: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str, dict - :raises: SerializationError if serialization fails. + :raises SerializationError: if serialization fails. :returns: The serialized data. """ key_transformer = kwargs.get("key_transformer", self.key_transformer) @@ -736,8 +650,8 @@ def body(self, data, data_type, **kwargs): :param object data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: dict - :raises: SerializationError if serialization fails. - :raises: ValueError if data is None + :raises SerializationError: if serialization fails. + :raises ValueError: if data is None :returns: The serialized request body """ @@ -781,8 +695,8 @@ def url(self, name, data, data_type, **kwargs): :param str data_type: The type to be serialized from. :rtype: str :returns: The serialized URL path - :raises: TypeError if serialization fails. - :raises: ValueError if data is None + :raises TypeError: if serialization fails. + :raises ValueError: if data is None """ try: output = self.serialize_data(data, data_type, **kwargs) @@ -805,8 +719,8 @@ def query(self, name, data, data_type, **kwargs): :param object data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str, list - :raises: TypeError if serialization fails. - :raises: ValueError if data is None + :raises TypeError: if serialization fails. + :raises ValueError: if data is None :returns: The serialized query parameter """ try: @@ -835,8 +749,8 @@ def header(self, name, data, data_type, **kwargs): :param object data: The data to be serialized. :param str data_type: The type to be serialized from. :rtype: str - :raises: TypeError if serialization fails. - :raises: ValueError if data is None + :raises TypeError: if serialization fails. + :raises ValueError: if data is None :returns: The serialized header """ try: @@ -855,9 +769,9 @@ def serialize_data(self, data, data_type, **kwargs): :param object data: The data to be serialized. :param str data_type: The type to be serialized from. - :raises: AttributeError if required data is None. - :raises: ValueError if data is None - :raises: SerializationError if serialization fails. + :raises AttributeError: if required data is None. + :raises ValueError: if data is None + :raises SerializationError: if serialization fails. :returns: The serialized data. :rtype: str, int, float, bool, dict, list """ @@ -1192,7 +1106,7 @@ def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument :param Datetime attr: Object to be serialized. :rtype: str - :raises: TypeError if format invalid. + :raises TypeError: if format invalid. :return: serialized rfc """ try: @@ -1218,7 +1132,7 @@ def serialize_iso(attr, **kwargs): # pylint: disable=unused-argument :param Datetime attr: Object to be serialized. :rtype: str - :raises: SerializationError if format invalid. + :raises SerializationError: if format invalid. :return: serialized iso """ if isinstance(attr, str): @@ -1251,7 +1165,7 @@ def serialize_unix(attr, **kwargs): # pylint: disable=unused-argument :param Datetime attr: Object to be serialized. :rtype: int - :raises: SerializationError if format invalid + :raises SerializationError: if format invalid :return: serialied unix """ if isinstance(attr, int): @@ -1429,7 +1343,7 @@ def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument # Iter and wrapped, should have found one node only (the wrap one) if len(children) != 1: raise DeserializationError( - "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( # pylint: disable=line-too-long + "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( xml_name ) ) @@ -1488,7 +1402,7 @@ def __call__(self, target_obj, response_data, content_type=None): :param str target_obj: Target data type to deserialize to. :param requests.Response response_data: REST response object. :param str content_type: Swagger "produces" if available. - :raises: DeserializationError if deserialization fails. + :raises DeserializationError: if deserialization fails. :return: Deserialized object. :rtype: object """ @@ -1502,7 +1416,7 @@ def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return :param str target_obj: Target data type to deserialize to. :param object data: Object to deserialize. - :raises: DeserializationError if deserialization fails. + :raises DeserializationError: if deserialization fails. :return: Deserialized object. :rtype: object """ @@ -1717,7 +1631,7 @@ def deserialize_data(self, data, data_type): # pylint: disable=too-many-return- :param str data: The response string to be deserialized. :param str data_type: The type to deserialize to. - :raises: DeserializationError if deserialization fails. + :raises DeserializationError: if deserialization fails. :return: Deserialized object. :rtype: object """ @@ -1799,7 +1713,7 @@ def deserialize_object(self, attr, **kwargs): # pylint: disable=too-many-return :param dict attr: Dictionary to be deserialized. :return: Deserialized object. :rtype: dict - :raises: TypeError if non-builtin datatype encountered. + :raises TypeError: if non-builtin datatype encountered. """ if attr is None: return None @@ -1845,7 +1759,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. """ # If we're here, data is supposed to be a basic type. # If it's still an XML node, take the text @@ -1936,7 +1850,7 @@ def deserialize_bytearray(attr): :param str attr: response string to be deserialized. :return: Deserialized bytearray :rtype: bytearray - :raises: TypeError if string format invalid. + :raises TypeError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1949,7 +1863,7 @@ def deserialize_base64(attr): :param str attr: response string to be deserialized. :return: Deserialized base64 string :rtype: bytearray - :raises: TypeError if string format invalid. + :raises TypeError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1964,7 +1878,7 @@ def deserialize_decimal(attr): :param str attr: response string to be deserialized. :return: Deserialized decimal - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. :rtype: decimal """ if isinstance(attr, ET.Element): @@ -1982,7 +1896,7 @@ def deserialize_long(attr): :param str attr: response string to be deserialized. :return: Deserialized int :rtype: long or int - :raises: ValueError if string format invalid. + :raises ValueError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -1995,7 +1909,7 @@ def deserialize_duration(attr): :param str attr: response string to be deserialized. :return: Deserialized duration :rtype: TimeDelta - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2013,7 +1927,7 @@ def deserialize_date(attr): :param str attr: response string to be deserialized. :return: Deserialized date :rtype: Date - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2029,7 +1943,7 @@ def deserialize_time(attr): :param str attr: response string to be deserialized. :return: Deserialized time :rtype: datetime.time - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2044,14 +1958,14 @@ def deserialize_rfc(attr): :param str attr: response string to be deserialized. :return: Deserialized RFC datetime :rtype: Datetime - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text try: parsed_date = email.utils.parsedate_tz(attr) # type: ignore date_obj = datetime.datetime( - *parsed_date[:6], tzinfo=_FixedOffset(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) + *parsed_date[:6], tzinfo=datetime.timezone(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) ) if not date_obj.tzinfo: date_obj = date_obj.astimezone(tz=TZ_UTC) @@ -2067,7 +1981,7 @@ def deserialize_iso(attr): :param str attr: response string to be deserialized. :return: Deserialized ISO datetime :rtype: Datetime - :raises: DeserializationError if string format invalid. + :raises DeserializationError: if string format invalid. """ if isinstance(attr, ET.Element): attr = attr.text @@ -2105,7 +2019,7 @@ def deserialize_unix(attr): :param int attr: Object to be serialized. :return: Deserialized datetime :rtype: Datetime - :raises: DeserializationError if format invalid + :raises DeserializationError: if format invalid """ if isinstance(attr, ET.Element): attr = int(attr.text) # type: ignore diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_vendor.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/utils.py similarity index 60% rename from sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_vendor.py rename to sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/utils.py index 1e41de430fa6..35c9c836f85f 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_vendor.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/_utils/utils.py @@ -1,25 +1,25 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from abc import ABC -from typing import TYPE_CHECKING - -from ._configuration import LogsIngestionClientConfiguration +from typing import Generic, TYPE_CHECKING, TypeVar if TYPE_CHECKING: - from azure.core import PipelineClient + from .serialization import Deserializer, Serializer + - from ._serialization import Deserializer, Serializer +TClient = TypeVar("TClient") +TConfig = TypeVar("TConfig") -class LogsIngestionClientMixinABC(ABC): +class ClientMixinABC(ABC, Generic[TClient, TConfig]): """DO NOT use this class. It is for internal typing use only.""" - _client: "PipelineClient" - _config: LogsIngestionClientConfiguration + _client: TClient + _config: TConfig _serialize: "Serializer" _deserialize: "Deserializer" diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/__init__.py index 320e9412988f..6273743bd569 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/__init__.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/__init__.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- # pylint: disable=wrong-import-position diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_client.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_client.py index 790c3684c952..671a152a921a 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_client.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_client.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- @@ -14,7 +14,7 @@ from azure.core.pipeline import policies from azure.core.rest import AsyncHttpResponse, HttpRequest -from .._serialization import Deserializer, Serializer +from .._utils.serialization import Deserializer, Serializer from ._configuration import LogsIngestionClientConfiguration from ._operations import LogsIngestionClientOperationsMixin @@ -23,21 +23,23 @@ class LogsIngestionClient(LogsIngestionClientOperationsMixin): - """Azure Monitor Data Collection Python Client. + """Azure Monitor data collection client. - :param endpoint: The Data Collection Endpoint for the Data Collection Rule, for example - https://dce-name.eastus-2.ingest.monitor.azure.com. Required. + :param endpoint: The Data Collection Endpoint for the Data Collection Rule. For example, + `https://dce-name.eastus-2.ingest.monitor.azure.com + `_. Required. :type endpoint: str - :param credential: Credential needed for the client to connect to Azure. Required. + :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: Api Version. Default value is "2023-01-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Default value is "2023-01-01". + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ def __init__(self, endpoint: str, credential: "AsyncTokenCredential", **kwargs: Any) -> None: _endpoint = "{endpoint}" self._config = LogsIngestionClientConfiguration(endpoint=endpoint, credential=credential, **kwargs) + _policies = kwargs.pop("policies", None) if _policies is None: _policies = [ diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_configuration.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_configuration.py index 4f5fa166407f..53eff028410e 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_configuration.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_configuration.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- @@ -10,11 +10,11 @@ from azure.core.pipeline import policies +from .._version import VERSION + if TYPE_CHECKING: from azure.core.credentials_async import AsyncTokenCredential -VERSION = "unknown" - class LogsIngestionClientConfiguration: # pylint: disable=too-many-instance-attributes """Configuration for LogsIngestionClient. @@ -22,13 +22,14 @@ class LogsIngestionClientConfiguration: # pylint: disable=too-many-instance-att Note that all parameters used to create this instance are saved as instance attributes. - :param endpoint: The Data Collection Endpoint for the Data Collection Rule, for example - https://dce-name.eastus-2.ingest.monitor.azure.com. Required. + :param endpoint: The Data Collection Endpoint for the Data Collection Rule. For example, + `https://dce-name.eastus-2.ingest.monitor.azure.com + `_. Required. :type endpoint: str - :param credential: Credential needed for the client to connect to Azure. Required. + :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: Api Version. Default value is "2023-01-01". Note that overriding this - default value may result in unsupported behavior. + :keyword api_version: The API version to use for this operation. Default value is "2023-01-01". + Note that overriding this default value may result in unsupported behavior. :paramtype api_version: str """ @@ -43,7 +44,7 @@ def __init__(self, endpoint: str, credential: "AsyncTokenCredential", **kwargs: self.endpoint = endpoint self.credential = credential self.api_version = api_version - self.credential_scopes = kwargs.pop("credential_scopes", ["https://monitor.azure.com//.default"]) + self.credential_scopes = kwargs.pop("credential_scopes", ["https://monitor.azure.com/.default"]) kwargs.setdefault("sdk_moniker", "monitor-ingestion/{}".format(VERSION)) self.polling_interval = kwargs.get("polling_interval", 30) self._configure(**kwargs) diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/__init__.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/__init__.py index 4bf65f393a39..34ad2ded0df9 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/__init__.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/__init__.py @@ -2,7 +2,7 @@ # -------------------------------------------------------------------------- # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- # pylint: disable=wrong-import-position diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_operations.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_operations.py index 523b9c603bca..3608fb82461e 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_operations.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_operations.py @@ -1,14 +1,17 @@ +# pylint: disable=line-too-long,useless-suppression # 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) AutoRest Code Generator. +# Code generated by Microsoft (R) Python Code Generator. # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from collections.abc import MutableMapping from io import IOBase -import sys +import json from typing import Any, Callable, Dict, IO, List, Optional, TypeVar, Union, overload +from azure.core import AsyncPipelineClient from azure.core.exceptions import ( ClientAuthenticationError, HttpResponseError, @@ -23,25 +26,24 @@ from azure.core.utils import case_insensitive_dict from ..._operations._operations import build_logs_ingestion_upload_request -from .._vendor import LogsIngestionClientMixinABC +from ..._utils.model_base import SdkJSONEncoder +from ..._utils.utils import ClientMixinABC +from .._configuration import LogsIngestionClientConfiguration -if sys.version_info >= (3, 9): - from collections.abc import MutableMapping -else: - from typing import MutableMapping # type: ignore -JSON = MutableMapping[str, Any] # pylint: disable=unsubscriptable-object T = TypeVar("T") ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] -class LogsIngestionClientOperationsMixin(LogsIngestionClientMixinABC): +class LogsIngestionClientOperationsMixin( + ClientMixinABC[AsyncPipelineClient[HttpRequest, AsyncHttpResponse], LogsIngestionClientConfiguration] +): @overload async def _upload( self, rule_id: str, - stream: str, - body: List[JSON], + stream_name: str, + body: List[Dict[str, Any]], *, content_encoding: Optional[str] = None, content_type: str = "application/json", @@ -51,7 +53,7 @@ async def _upload( async def _upload( self, rule_id: str, - stream: str, + stream_name: str, body: IO[bytes], *, content_encoding: Optional[str] = None, @@ -63,24 +65,26 @@ async def _upload( async def _upload( self, rule_id: str, - stream: str, - body: Union[List[JSON], IO[bytes]], + stream_name: str, + body: Union[List[Dict[str, Any]], IO[bytes]], *, content_encoding: Optional[str] = None, **kwargs: Any ) -> None: """Ingestion API used to directly ingest data using Data Collection Rules. - See error response code and error response message for more detail. + Ingestion API used to directly ingest data using Data Collection Rules. - :param rule_id: The immutable Id of the Data Collection Rule resource. Required. + :param rule_id: The immutable ID of the Data Collection Rule resource. Required. :type rule_id: str - :param stream: The streamDeclaration name as defined in the Data Collection Rule. Required. - :type stream: str - :param body: An array of objects matching the schema defined by the provided stream. Is either - a [JSON] type or a IO[bytes] type. Required. - :type body: list[JSON] or IO[bytes] - :keyword content_encoding: gzip. Default value is None. + :param stream_name: The streamDeclaration name as defined in the Data Collection Rule. + Required. + :type stream_name: str + :param body: The array of objects matching the schema defined by the provided stream. Is either + a [{str: Any}] type or a IO[bytes] type. Required. + :type body: list[dict[str, any]] or IO[bytes] + :keyword content_encoding: The content encoding of the request body which is always 'gzip'. + Default value is None. :paramtype content_encoding: str :return: None :rtype: None @@ -101,20 +105,18 @@ async def _upload( cls: ClsType[None] = kwargs.pop("cls", None) content_type = content_type or "application/json" - _json = None _content = None if isinstance(body, (IOBase, bytes)): _content = body else: - _json = body + _content = json.dumps(body, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore _request = build_logs_ingestion_upload_request( rule_id=rule_id, - stream=stream, + stream_name=stream_name, content_encoding=content_encoding, content_type=content_type, api_version=self._config.api_version, - json=_json, content=_content, headers=_headers, params=_params, diff --git a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_patch.py b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_patch.py index 42a953738fe3..16c5ecfe6b23 100644 --- a/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_patch.py +++ b/sdk/monitor/azure-monitor-ingestion/azure/monitor/ingestion/aio/_operations/_patch.py @@ -65,7 +65,13 @@ async def upload( content_encoding = "gzip" logs.seek(0) - await super()._upload(rule_id, stream=stream_name, body=logs, content_encoding=content_encoding, **kwargs) + await super()._upload( + rule_id, + stream_name=stream_name, + body=cast(IO[bytes], logs), + content_encoding=content_encoding, + **kwargs + ) return if not isinstance(logs, Sequence) or isinstance(logs, str): @@ -76,7 +82,7 @@ async def upload( for gzip_data, log_chunk in _create_gzip_requests(cast(List[JSON], logs)): try: await super()._upload( # type: ignore - rule_id, stream=stream_name, body=gzip_data, content_encoding="gzip", **kwargs # type: ignore + rule_id, stream_name=stream_name, body=gzip_data, content_encoding="gzip", **kwargs # type: ignore ) except Exception as err: # pylint: disable=broad-except diff --git a/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_file_contents_async.py b/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_file_contents_async.py index 2b9344a63d40..d4e3b3b0e588 100644 --- a/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_file_contents_async.py +++ b/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_file_contents_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for diff --git a/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_pandas_dataframe_async.py b/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_pandas_dataframe_async.py index 1cb0ed847d9f..240e40f5ae67 100644 --- a/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_pandas_dataframe_async.py +++ b/sdk/monitor/azure-monitor-ingestion/samples/async_samples/sample_upload_pandas_dataframe_async.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,useless-suppression # ------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for diff --git a/sdk/monitor/azure-monitor-ingestion/tests/conftest.py b/sdk/monitor/azure-monitor-ingestion/tests/conftest.py index f2fdaab5cfd2..560052fcbbcc 100644 --- a/sdk/monitor/azure-monitor-ingestion/tests/conftest.py +++ b/sdk/monitor/azure-monitor-ingestion/tests/conftest.py @@ -67,7 +67,8 @@ def add_sanitizers(test_proxy, environment_variables): compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" ) add_general_regex_sanitizer( - value="fakeresource", regex="(?<=\\/\\/)[a-z-]+(?=\\.westus2-1\\.ingest\\.monitor\\.azure\\.com)" + regex="http[s]?://.+\\.ingest\\.monitor\\.azure\\.com", + value="http://fakeresource.ingest.monitor.azure.com", ) add_body_key_sanitizer(json_path="access_token", value="fakekey") add_header_regex_sanitizer(key="Set-Cookie", value="[set-cookie;]") diff --git a/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion.py b/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion.py index dfeb9f7adacb..40414068ed6c 100644 --- a/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion.py +++ b/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion.py @@ -83,7 +83,7 @@ def test_send_logs_json_file(self, recorded_test, monitor_info): json.dump(LOGS_BODY, f) with client: - with open(temp_file, "r") as f: + with open(temp_file, "rb") as f: client.upload(rule_id=monitor_info["dcr_id"], stream_name=monitor_info["stream_name"], logs=f) os.remove(temp_file) diff --git a/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion_async.py b/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion_async.py index f025d9693d7c..de0c5f982f98 100644 --- a/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion_async.py +++ b/sdk/monitor/azure-monitor-ingestion/tests/test_logs_ingestion_async.py @@ -93,7 +93,7 @@ async def test_send_logs_json_file(self, recorded_test, monitor_info): json.dump(LOGS_BODY, f) async with client: - with open(temp_file, "r") as f: + with open(temp_file, "rb") as f: await client.upload(rule_id=monitor_info["dcr_id"], stream_name=monitor_info["stream_name"], logs=f) os.remove(temp_file) await credential.close() diff --git a/sdk/monitor/azure-monitor-ingestion/tsp-location.yaml b/sdk/monitor/azure-monitor-ingestion/tsp-location.yaml new file mode 100644 index 000000000000..4676c5213e38 --- /dev/null +++ b/sdk/monitor/azure-monitor-ingestion/tsp-location.yaml @@ -0,0 +1,4 @@ +directory: specification/monitor/Azure.Monitor.Ingestion +commit: bd99a78116dcf58af8356838f10a86de2f8b1ed4 +repo: Azure/azure-rest-api-specs +additionalDirectories: