1# pylint: disable=line-too-long,useless-suppression,too-many-lines
2# coding=utf-8
3# --------------------------------------------------------------------------
4# Copyright (c) Microsoft Corporation. All rights reserved.
5# Licensed under the MIT License. See License.txt in the project root for license information.
6# Code generated by Microsoft (R) Python Code Generator.
7# Changes may cause incorrect behavior and will be lost if the code is regenerated.
8# --------------------------------------------------------------------------
9# pylint: disable=protected-access, broad-except
10
11import copy
12import calendar
13import decimal
14import functools
15import sys
16import logging
17import base64
18import re
19import typing
20import enum
21import email.utils
22from datetime import datetime, date, time, timedelta, timezone
23from json import JSONEncoder
24import xml.etree.ElementTree as ET
25from collections.abc import MutableMapping
26import isodate
27from azure.core.exceptions import DeserializationError
28from azure.core import CaseInsensitiveEnumMeta
29from azure.core.pipeline import PipelineResponse
30from azure.core.serialization import _Null
31
32from azure.core.rest import HttpResponse
33
34if sys.version_info >= (3, 11):
35 from typing import Self
36else:
37 from typing_extensions import Self
38
39_LOGGER = logging.getLogger(__name__)
40
41__all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"]
42
43TZ_UTC = timezone.utc
44_T = typing.TypeVar("_T")
45_NONE_TYPE = type(None)
46
47
48def _timedelta_as_isostr(td: timedelta) -> str:
49 """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S'
50
51 Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython
52
53 :param timedelta td: The timedelta to convert
54 :rtype: str
55 :return: ISO8601 version of this timedelta
56 """
57
58 # Split seconds to larger units
59 seconds = td.total_seconds()
60 minutes, seconds = divmod(seconds, 60)
61 hours, minutes = divmod(minutes, 60)
62 days, hours = divmod(hours, 24)
63
64 days, hours, minutes = list(map(int, (days, hours, minutes)))
65 seconds = round(seconds, 6)
66
67 # Build date
68 date_str = ""
69 if days:
70 date_str = "%sD" % days
71
72 if hours or minutes or seconds:
73 # Build time
74 time_str = "T"
75
76 # Hours
77 bigger_exists = date_str or hours
78 if bigger_exists:
79 time_str += "{:02}H".format(hours)
80
81 # Minutes
82 bigger_exists = bigger_exists or minutes
83 if bigger_exists:
84 time_str += "{:02}M".format(minutes)
85
86 # Seconds
87 try:
88 if seconds.is_integer():
89 seconds_string = "{:02}".format(int(seconds))
90 else:
91 # 9 chars long w/ leading 0, 6 digits after decimal
92 seconds_string = "%09.6f" % seconds
93 # Remove trailing zeros
94 seconds_string = seconds_string.rstrip("0")
95 except AttributeError: # int.is_integer() raises
96 seconds_string = "{:02}".format(seconds)
97
98 time_str += "{}S".format(seconds_string)
99 else:
100 time_str = ""
101
102 return "P" + date_str + time_str
103
104
105def _serialize_bytes(o, format: typing.Optional[str] = None) -> str:
106 encoded = base64.b64encode(o).decode()
107 if format == "base64url":
108 return encoded.strip("=").replace("+", "-").replace("/", "_")
109 return encoded
110
111
112def _serialize_duration(td: timedelta, format: typing.Optional[str] = None):
113 """Serialize a timedelta to its wire representation.
114
115 For the ``seconds``/``milliseconds`` encodings the value is converted to a
116 numeric value, otherwise it falls back to an ISO 8601 duration string.
117
118 :param timedelta td: The timedelta to serialize.
119 :param str format: The duration encoding format.
120 :rtype: int or float or str
121 :return: serialized duration
122 """
123 seconds = td.total_seconds()
124 if format == "duration-seconds-int":
125 return int(seconds)
126 if format == "duration-seconds-float":
127 return seconds
128 if format == "duration-milliseconds-int":
129 return int(seconds * 1000)
130 if format == "duration-milliseconds-float":
131 return seconds * 1000
132 return _timedelta_as_isostr(td)
133
134
135def _serialize_datetime(o, format: typing.Optional[str] = None):
136 if hasattr(o, "year") and hasattr(o, "hour"):
137 if format == "rfc7231":
138 return email.utils.format_datetime(o, usegmt=True)
139 if format == "unix-timestamp":
140 return int(calendar.timegm(o.utctimetuple()))
141
142 # astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set)
143 if not o.tzinfo:
144 iso_formatted = o.replace(tzinfo=TZ_UTC).isoformat()
145 else:
146 iso_formatted = o.astimezone(TZ_UTC).isoformat()
147 # Replace the trailing "+00:00" UTC offset with "Z" (RFC 3339: https://www.ietf.org/rfc/rfc3339.txt)
148 return iso_formatted.replace("+00:00", "Z")
149 # Next try datetime.date or datetime.time
150 return o.isoformat()
151
152
153def _is_readonly(p):
154 try:
155 return p._visibility == ["read"]
156 except AttributeError:
157 return False
158
159
160class SdkJSONEncoder(JSONEncoder):
161 """A JSON encoder that's capable of serializing datetime objects and bytes."""
162
163 def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs):
164 super().__init__(*args, **kwargs)
165 self.exclude_readonly = exclude_readonly
166 self.format = format
167
168 def default(self, o): # pylint: disable=too-many-return-statements
169 if _is_model(o):
170 if self.exclude_readonly:
171 readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)]
172 return {k: v for k, v in o.items() if k not in readonly_props}
173 return dict(o.items())
174 try:
175 return super(SdkJSONEncoder, self).default(o)
176 except TypeError:
177 if isinstance(o, _Null):
178 return None
179 if isinstance(o, decimal.Decimal):
180 return float(o)
181 if isinstance(o, (bytes, bytearray)):
182 return _serialize_bytes(o, self.format)
183 try:
184 # First try datetime.datetime
185 return _serialize_datetime(o, self.format)
186 except AttributeError:
187 pass
188 # Last, try datetime.timedelta
189 try:
190 return _timedelta_as_isostr(o)
191 except AttributeError:
192 # This will be raised when it hits value.total_seconds in the method above
193 pass
194 return super(SdkJSONEncoder, self).default(o)
195
196
197_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}]?")
198_VALID_RFC7231 = re.compile(
199 r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s"
200 r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT"
201)
202
203_ARRAY_ENCODE_MAPPING = {
204 "pipeDelimited": "|",
205 "spaceDelimited": " ",
206 "commaDelimited": ",",
207 "newlineDelimited": "\n",
208}
209
210
211def _deserialize_array_encoded(delimit: str, attr):
212 if isinstance(attr, str):
213 if attr == "":
214 return []
215 return attr.split(delimit)
216 return attr
217
218
219def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
220 """Deserialize ISO-8601 formatted string into Datetime object.
221
222 :param str attr: response string to be deserialized.
223 :rtype: ~datetime.datetime
224 :returns: The datetime object from that input
225 """
226 if isinstance(attr, datetime):
227 # i'm already deserialized
228 return attr
229 attr = attr.upper()
230 match = _VALID_DATE.match(attr)
231 if not match:
232 raise ValueError("Invalid datetime string: " + attr)
233
234 check_decimal = attr.split(".")
235 if len(check_decimal) > 1:
236 decimal_str = ""
237 for digit in check_decimal[1]:
238 if digit.isdigit():
239 decimal_str += digit
240 else:
241 break
242 if len(decimal_str) > 6:
243 attr = attr.replace(decimal_str, decimal_str[0:6])
244
245 date_obj = isodate.parse_datetime(attr)
246 test_utc = date_obj.utctimetuple()
247 if test_utc.tm_year > 9999 or test_utc.tm_year < 1:
248 raise OverflowError("Hit max or min date")
249 return date_obj # type: ignore[no-any-return]
250
251
252def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime:
253 """Deserialize RFC7231 formatted string into Datetime object.
254
255 :param str attr: response string to be deserialized.
256 :rtype: ~datetime.datetime
257 :returns: The datetime object from that input
258 """
259 if isinstance(attr, datetime):
260 # i'm already deserialized
261 return attr
262 match = _VALID_RFC7231.match(attr)
263 if not match:
264 raise ValueError("Invalid datetime string: " + attr)
265
266 return email.utils.parsedate_to_datetime(attr)
267
268
269def _deserialize_datetime_unix_timestamp(attr: typing.Union[float, datetime]) -> datetime:
270 """Deserialize unix timestamp into Datetime object.
271
272 :param str attr: response string to be deserialized.
273 :rtype: ~datetime.datetime
274 :returns: The datetime object from that input
275 """
276 if isinstance(attr, datetime):
277 # i'm already deserialized
278 return attr
279 return datetime.fromtimestamp(attr, TZ_UTC)
280
281
282def _deserialize_date(attr: typing.Union[str, date]) -> date:
283 """Deserialize ISO-8601 formatted string into Date object.
284 :param str attr: response string to be deserialized.
285 :rtype: date
286 :returns: The date object from that input
287 """
288 # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception.
289 if isinstance(attr, date):
290 return attr
291 return isodate.parse_date(attr, defaultmonth=None, defaultday=None) # type: ignore
292
293
294def _deserialize_time(attr: typing.Union[str, time]) -> time:
295 """Deserialize ISO-8601 formatted string into time object.
296
297 :param str attr: response string to be deserialized.
298 :rtype: datetime.time
299 :returns: The time object from that input
300 """
301 if isinstance(attr, time):
302 return attr
303 return isodate.parse_time(attr) # type: ignore[no-any-return]
304
305
306def _deserialize_bytes(attr):
307 if isinstance(attr, (bytes, bytearray)):
308 return attr
309 return bytes(base64.b64decode(attr))
310
311
312def _deserialize_bytes_base64(attr):
313 if isinstance(attr, (bytes, bytearray)):
314 return attr
315 padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore
316 attr = attr + padding # type: ignore
317 encoded = attr.replace("-", "+").replace("_", "/")
318 return bytes(base64.b64decode(encoded))
319
320
321def _deserialize_duration(attr):
322 if isinstance(attr, timedelta):
323 return attr
324 return isodate.parse_duration(attr)
325
326
327def _deserialize_duration_numeric(attr, unit):
328 if isinstance(attr, timedelta):
329 return attr
330 return timedelta(**{unit: float(attr)})
331
332
333def _deserialize_decimal(attr):
334 if isinstance(attr, decimal.Decimal):
335 return attr
336 return decimal.Decimal(str(attr))
337
338
339def _deserialize_int_as_str(attr):
340 if isinstance(attr, int):
341 return attr
342 return int(attr)
343
344
345_DESERIALIZE_MAPPING = {
346 datetime: _deserialize_datetime,
347 date: _deserialize_date,
348 time: _deserialize_time,
349 bytes: _deserialize_bytes,
350 bytearray: _deserialize_bytes,
351 timedelta: _deserialize_duration,
352 typing.Any: lambda x: x,
353 decimal.Decimal: _deserialize_decimal,
354}
355
356_DESERIALIZE_MAPPING_WITHFORMAT = {
357 "rfc3339": _deserialize_datetime,
358 "rfc7231": _deserialize_datetime_rfc7231,
359 "unix-timestamp": _deserialize_datetime_unix_timestamp,
360 "base64": _deserialize_bytes,
361 "base64url": _deserialize_bytes_base64,
362 "duration-seconds-int": functools.partial(_deserialize_duration_numeric, unit="seconds"),
363 "duration-seconds-float": functools.partial(_deserialize_duration_numeric, unit="seconds"),
364 "duration-milliseconds-int": functools.partial(_deserialize_duration_numeric, unit="milliseconds"),
365 "duration-milliseconds-float": functools.partial(_deserialize_duration_numeric, unit="milliseconds"),
366}
367
368
369def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
370 if annotation is int and rf and rf._format == "str":
371 return _deserialize_int_as_str
372 if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING:
373 return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format])
374 if rf and rf._format:
375 return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format)
376 return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore
377
378
379def _get_type_alias_type(module_name: str, alias_name: str):
380 types = {
381 k: v
382 for k, v in sys.modules[module_name].__dict__.items()
383 if isinstance(v, typing._GenericAlias) # type: ignore
384 }
385 if alias_name not in types:
386 return alias_name
387 return types[alias_name]
388
389
390def _get_model(module_name: str, model_name: str):
391 models = {k: v for k, v in sys.modules[module_name].__dict__.items() if isinstance(v, type)}
392 module_end = module_name.rsplit(".", 1)[0]
393 models.update({k: v for k, v in sys.modules[module_end].__dict__.items() if isinstance(v, type)})
394 if isinstance(model_name, str):
395 model_name = model_name.split(".")[-1]
396 if model_name not in models:
397 return model_name
398 return models[model_name]
399
400
401_UNSET = object()
402
403
404class _MyMutableMapping(MutableMapping[str, typing.Any]):
405 def __init__(self, data: dict[str, typing.Any]) -> None:
406 self._data = data
407
408 def __contains__(self, key: typing.Any) -> bool:
409 return key in self._data
410
411 def __getitem__(self, key: str) -> typing.Any:
412 # If this key has been deserialized (for mutable types), we need to handle serialization
413 if hasattr(self, "_attr_to_rest_field"):
414 cache_attr = f"_deserialized_{key}"
415 if hasattr(self, cache_attr):
416 rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key)
417 if rf:
418 value = self._data.get(key)
419 if isinstance(value, (dict, list, set)):
420 # For mutable types, serialize and return
421 # But also update _data with serialized form and clear flag
422 # so mutations via this returned value affect _data
423 serialized = _serialize(value, rf._format)
424 # If serialized form is same type (no transformation needed),
425 # return _data directly so mutations work
426 if isinstance(serialized, type(value)) and serialized == value:
427 return self._data.get(key)
428 # Otherwise return serialized copy and clear flag
429 try:
430 object.__delattr__(self, cache_attr)
431 except AttributeError:
432 pass
433 # Store serialized form back
434 self._data[key] = serialized
435 return serialized
436 return self._data.__getitem__(key)
437
438 def __setitem__(self, key: str, value: typing.Any) -> None:
439 # Clear any cached deserialized value when setting through dictionary access
440 cache_attr = f"_deserialized_{key}"
441 try:
442 object.__delattr__(self, cache_attr)
443 except AttributeError:
444 pass
445 self._data.__setitem__(key, value)
446
447 def __delitem__(self, key: str) -> None:
448 self._data.__delitem__(key)
449
450 def __iter__(self) -> typing.Iterator[typing.Any]:
451 return self._data.__iter__()
452
453 def __len__(self) -> int:
454 return self._data.__len__()
455
456 def __ne__(self, other: typing.Any) -> bool:
457 return not self.__eq__(other)
458
459 def keys(self) -> typing.KeysView[str]:
460 """
461 :returns: a set-like object providing a view on the mapping's keys
462 :rtype: ~typing.KeysView
463 """
464 return self._data.keys()
465
466 def values(self) -> typing.ValuesView[typing.Any]:
467 """
468 :returns: an object providing a view on the mapping's values
469 :rtype: ~typing.ValuesView
470 """
471 return self._data.values()
472
473 def items(self) -> typing.ItemsView[str, typing.Any]:
474 """
475 :returns: a set-like object providing a view on the mapping's items
476 :rtype: ~typing.ItemsView
477 """
478 return self._data.items()
479
480 def get(self, key: str, default: typing.Any = None) -> typing.Any:
481 """
482 Get the value for key if key is in the dictionary, else default.
483 :param str key: The key to look up.
484 :param any default: The value to return if key is not in the dictionary. Defaults to None
485 :returns: The value for key if key is in the dictionary, else default.
486 :rtype: any
487 """
488 try:
489 return self[key]
490 except KeyError:
491 return default
492
493 @typing.overload
494 def pop(self, key: str) -> typing.Any: ... # pylint: disable=arguments-differ
495
496 @typing.overload
497 def pop(self, key: str, default: _T) -> _T: ... # pylint: disable=signature-differs
498
499 @typing.overload
500 def pop(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs
501
502 def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any:
503 """
504 Removes specified key and return the corresponding value.
505 :param str key: The key to pop.
506 :param any default: The value to return if key is not in the dictionary
507 :returns: The value corresponding to the key.
508 :rtype: any
509 :raises KeyError: If key is not found and default is not given.
510 """
511 if default is _UNSET:
512 return self._data.pop(key)
513 return self._data.pop(key, default)
514
515 def popitem(self) -> tuple[str, typing.Any]:
516 """
517 Removes and returns some (key, value) pair
518 :returns: The (key, value) pair.
519 :rtype: tuple
520 :raises KeyError: if the dictionary is empty.
521 """
522 return self._data.popitem()
523
524 def clear(self) -> None:
525 """
526 Remove all items from the dictionary.
527 """
528 self._data.clear()
529
530 def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: # pylint: disable=arguments-differ
531 """
532 Update the dictionary from a mapping or an iterable of key-value pairs.
533 :param any args: Either a mapping object or an iterable of key-value pairs.
534 """
535 self._data.update(*args, **kwargs)
536
537 @typing.overload
538 def setdefault(self, key: str, default: None = None) -> None: ...
539
540 @typing.overload
541 def setdefault(self, key: str, default: typing.Any) -> typing.Any: ... # pylint: disable=signature-differs
542
543 def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any:
544 """
545 Return the value for key if key is in the dictionary; otherwise set the key to
546 default and return default.
547 :param str key: The key to look up.
548 :param any default: The value to set if key is not in the dictionary
549 :returns: The value for key if key is in the dictionary, else default.
550 :rtype: any
551 """
552 if default is _UNSET:
553 return self._data.setdefault(key)
554 return self._data.setdefault(key, default)
555
556 def __eq__(self, other: typing.Any) -> bool:
557 if isinstance(other, _MyMutableMapping):
558 return self._data == other._data
559 try:
560 other_model = self.__class__(other)
561 except Exception:
562 return False
563 return self._data == other_model._data
564
565 def __repr__(self) -> str:
566 return str(self._data)
567
568
569def _is_model(obj: typing.Any) -> bool:
570 return getattr(obj, "_is_model", False)
571
572
573def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements
574 if isinstance(o, list):
575 if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o):
576 return _ARRAY_ENCODE_MAPPING[format].join(o)
577 return [_serialize(x, format) for x in o]
578 if isinstance(o, dict):
579 return {k: _serialize(v, format) for k, v in o.items()}
580 if isinstance(o, set):
581 return {_serialize(x, format) for x in o}
582 if isinstance(o, tuple):
583 return tuple(_serialize(x, format) for x in o)
584 if isinstance(o, (bytes, bytearray)):
585 return _serialize_bytes(o, format)
586 if isinstance(o, decimal.Decimal):
587 return float(o)
588 if isinstance(o, enum.Enum):
589 return o.value
590 if isinstance(o, int):
591 if format == "str":
592 return str(o)
593 return o
594 try:
595 # First try datetime.datetime
596 return _serialize_datetime(o, format)
597 except AttributeError:
598 pass
599 # Last, try datetime.timedelta
600 try:
601 return _serialize_duration(o, format)
602 except AttributeError:
603 # This will be raised when it hits value.total_seconds in the method above
604 pass
605 return o
606
607
608def _get_rest_field(attr_to_rest_field: dict[str, "_RestField"], rest_name: str) -> typing.Optional["_RestField"]:
609 try:
610 return next(rf for rf in attr_to_rest_field.values() if rf._rest_name == rest_name)
611 except StopIteration:
612 return None
613
614
615def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any:
616 if not rf:
617 return _serialize(value, None)
618 if rf._is_multipart_file_input:
619 return value
620 if rf._is_model:
621 return _deserialize(rf._type, value)
622 if isinstance(value, ET.Element):
623 value = _deserialize(rf._type, value)
624 return _serialize(value, rf._format)
625
626
627# ============================================================================
628# Fast-path scalar deserializer functions for rest_field(deserializer=...)
629# These are referenced from rest_field declarations to bypass the generic
630# _deserialize -> _deserialize_with_callable chain.
631# Only simple/primitive types — no models or container types.
632# ============================================================================
633
634
635def _xml_deser_str(value):
636 if isinstance(value, ET.Element):
637 return value.text or ""
638 return str(value) if value is not None else None
639
640
641def _xml_deser_int(value):
642 if isinstance(value, ET.Element):
643 return int(value.text) if value.text else None
644 return int(value) if value is not None else None
645
646
647def _xml_deser_float(value):
648 if isinstance(value, ET.Element):
649 return float(value.text) if value.text else None
650 return float(value) if value is not None else None
651
652
653def _xml_deser_bool(value):
654 if isinstance(value, ET.Element):
655 text = value.text
656 else:
657 text = value
658 if text is None:
659 return None
660 if text in (True, False):
661 return text
662 return text.lower() == "true"
663
664
665# pylint: disable=docstring-missing-param
666def _xml_deser_bytes(value):
667 """Deserialize bytes from XML (base64)."""
668 if isinstance(value, ET.Element):
669 text = value.text
670 else:
671 text = value
672 if text is None:
673 return None
674 return _deserialize_bytes(text)
675
676
677def _xml_deser_bytes_base64url(value):
678 """Deserialize bytes from XML (base64url)."""
679 if isinstance(value, ET.Element):
680 text = value.text
681 else:
682 text = value
683 if text is None:
684 return None
685 return _deserialize_bytes_base64(text)
686
687
688def _xml_deser_datetime(value):
689 """Deserialize a datetime from XML (ISO 8601 / rfc3339)."""
690 if isinstance(value, ET.Element):
691 text = value.text
692 else:
693 text = value
694 if text is None:
695 return None
696 return _deserialize_datetime(text)
697
698
699def _xml_deser_datetime_rfc7231(value):
700 """Deserialize a datetime from XML (RFC7231 format)."""
701 if isinstance(value, ET.Element):
702 text = value.text
703 else:
704 text = value
705 if text is None:
706 return None
707 return _deserialize_datetime_rfc7231(text)
708
709
710def _xml_deser_datetime_unix_timestamp(value):
711 """Deserialize a datetime from XML (Unix timestamp)."""
712 if isinstance(value, ET.Element):
713 text = value.text
714 else:
715 text = value
716 if text is None:
717 return None
718 return _deserialize_datetime_unix_timestamp(float(text))
719
720
721def _xml_deser_date(value):
722 """Deserialize a date from XML (ISO 8601)."""
723 if isinstance(value, ET.Element):
724 text = value.text
725 else:
726 text = value
727 if text is None:
728 return None
729 return _deserialize_date(text)
730
731
732def _xml_deser_time(value):
733 """Deserialize a time from XML (ISO 8601)."""
734 if isinstance(value, ET.Element):
735 text = value.text
736 else:
737 text = value
738 if text is None:
739 return None
740 return _deserialize_time(text)
741
742
743def _xml_deser_duration(value):
744 """Deserialize a timedelta from XML (ISO 8601 duration)."""
745 if isinstance(value, ET.Element):
746 text = value.text
747 else:
748 text = value
749 if text is None:
750 return None
751 return _deserialize_duration(text)
752
753
754def _xml_deser_decimal(value):
755 """Deserialize a Decimal from XML."""
756 if isinstance(value, ET.Element):
757 text = value.text
758 else:
759 text = value
760 if text is None:
761 return None
762 return _deserialize_decimal(text)
763
764
765def _xml_deser_enum_or_str(enum_cls, value):
766 """Deserialize a Union[EnumType, str] from XML."""
767 text = value.text if isinstance(value, ET.Element) else value
768 if text is None:
769 return None
770 try:
771 return enum_cls(text)
772 except ValueError:
773 return text
774
775
776def _extract_xml_model_type(rf_type):
777 """Extract the concrete Model class from a resolved rf._type partial chain.
778
779 Unwraps ``Optional[Model]`` and ``_deserialize_model(Model, ...)``
780 wrappers. Only handles Model and Optional[Model] — other composite
781 types (List, Dict, Union, etc.) return None and fall through to the
782 generic ``_deserialize`` path at runtime.
783 """
784 if rf_type is None:
785 return None
786 if isinstance(rf_type, type) and _is_model(rf_type):
787 return rf_type
788 if not isinstance(rf_type, functools.partial):
789 return None
790 func = rf_type.func
791 args = rf_type.args
792 if func is _deserialize_with_optional and args:
793 return _extract_xml_model_type(args[0])
794 if func is _deserialize_model and args:
795 cls = args[0]
796 return cls if isinstance(cls, type) and _is_model(cls) else None
797 return None
798
799
800def _build_xml_field_plan( # pylint: disable=docstring-missing-return, docstring-missing-rtype, unused-variable
801 cls, attr_to_rest_field: dict
802) -> list:
803 """Build a precomputed XML field plan for fast _init_from_xml iteration.
804
805 Called once per model class in __new__. Returns a list of tuples:
806 (rest_name, xml_name, kind, deser, rf_type, is_optional, items_name)
807
808 kind: 0=wrapped, 1=attribute, 2=unwrapped, 3=text
809
810 For Model and Optional[Model] fields that lack a scalar
811 ``_deserializer``, this function precomputes the Model class as the
812 deserializer so ``_init_from_xml`` can call ``ModelClass(element)``
813 directly instead of going through the expensive
814 ``_get_deserialize_callable_from_annotation`` chain at runtime.
815 """
816 model_meta = getattr(cls, "_xml", {})
817 model_ns = model_meta.get("ns") or model_meta.get("namespace")
818 plan = []
819
820 for rf in attr_to_rest_field.values():
821 prop_meta = getattr(rf, "_xml", {})
822 deser = rf._deserializer
823
824 xml_name = prop_meta.get("name", rf._rest_name)
825 xml_ns = _resolve_xml_ns(prop_meta, model_meta)
826 if xml_ns:
827 xml_name = "{" + xml_ns + "}" + xml_name
828
829 is_optional = rf._is_optional
830
831 # For Model / Optional[Model] fields without a scalar deserializer,
832 # precompute the Model class as the deserializer.
833 if deser is None and rf._type is not None:
834 model_cls = _extract_xml_model_type(rf._type)
835 if model_cls is not None:
836 deser = model_cls
837
838 if prop_meta.get("attribute", False):
839 plan.append((rf._rest_name, xml_name, 1, deser, rf._type, is_optional, None))
840 elif prop_meta.get("unwrapped", False):
841 items_name = prop_meta.get("itemsName")
842 if items_name:
843 items_ns = prop_meta.get("itemsNs")
844 if items_ns is not None:
845 xml_ns = items_ns
846 if xml_ns:
847 items_name = "{" + xml_ns + "}" + items_name
848 else:
849 items_name = xml_name
850 plan.append((rf._rest_name, xml_name, 2, deser, rf._type, is_optional, items_name))
851 elif prop_meta.get("text", False):
852 plan.append((rf._rest_name, xml_name, 3, deser, rf._type, is_optional, None))
853 else:
854 plan.append((rf._rest_name, xml_name, 0, deser, rf._type, is_optional, None))
855
856 return plan
857
858
859# pylint: enable=docstring-missing-param
860class Model(_MyMutableMapping):
861 _is_model = True
862 # label whether current class's _attr_to_rest_field has been calculated
863 # could not see _attr_to_rest_field directly because subclass inherits it from parent class
864 _calculated: set[str] = set()
865
866 def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
867 class_name = self.__class__.__name__
868 if len(args) > 1:
869 raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given")
870 dict_to_pass: dict[str, typing.Any] = {}
871 if args:
872 if isinstance(args[0], ET.Element):
873 dict_to_pass.update(self._init_from_xml(args[0]))
874 else:
875 dict_to_pass.update(
876 {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()}
877 )
878 else:
879 non_attr_kwargs = [k for k in kwargs if k not in self._attr_to_rest_field]
880 if non_attr_kwargs:
881 # actual type errors only throw the first wrong keyword arg they see, so following that.
882 raise TypeError(f"{class_name}.__init__() got an unexpected keyword argument '{non_attr_kwargs[0]}'")
883 dict_to_pass.update(
884 {
885 self._attr_to_rest_field[k]._rest_name: _create_value(self._attr_to_rest_field[k], v)
886 for k, v in kwargs.items()
887 if v is not None
888 }
889 )
890 # Apply client default values for fields the caller didn't set so that
891 # defaults are part of `_data` and therefore included during serialization.
892 for rf in self._attr_to_rest_field.values():
893 if rf._default is _UNSET:
894 continue
895 if rf._rest_name in dict_to_pass:
896 continue
897 dict_to_pass[rf._rest_name] = _create_value(rf, rf._default)
898 super().__init__(dict_to_pass)
899
900 def _init_from_xml( # pylint: disable=too-many-branches, too-many-statements
901 self, element: ET.Element
902 ) -> dict[str, typing.Any]:
903 """Deserialize an XML element into a dict mapping rest field names to values.
904
905 :param ET.Element element: The XML element to deserialize from.
906 :returns: A dictionary of rest_name to deserialized value pairs.
907 :rtype: dict
908 """
909 result: dict[str, typing.Any] = {}
910 existed_attr_keys: list[str] = []
911
912 field_plan = getattr(self, "_xml_field_plan", None)
913 if field_plan:
914 for rest_name, xml_name, kind, deser, rf_type, is_optional, items_name in field_plan:
915 if kind == 0: # wrapped element (most common)
916 item = element.find(xml_name)
917 if item is not None:
918 existed_attr_keys.append(xml_name)
919 if deser:
920 result[rest_name] = deser(item)
921 else:
922 result[rest_name] = _deserialize(rf_type, item)
923 elif kind == 1: # attribute
924 attr_val = element.get(xml_name)
925 if attr_val is not None:
926 existed_attr_keys.append(xml_name)
927 if deser:
928 result[rest_name] = deser(attr_val)
929 else:
930 result[rest_name] = attr_val
931 elif kind == 2: # unwrapped array
932 items = element.findall(items_name) # pyright: ignore
933 if len(items) > 0:
934 existed_attr_keys.append(items_name)
935 if deser:
936 result[rest_name] = deser(items)
937 else:
938 result[rest_name] = _deserialize(rf_type, items)
939 elif not is_optional:
940 existed_attr_keys.append(items_name)
941 result[rest_name] = []
942 elif kind == 3: # text
943 if element.text is not None:
944 if deser:
945 result[rest_name] = deser(element.text)
946 else:
947 result[rest_name] = element.text
948 else:
949 model_meta = getattr(self, "_xml", {})
950 for rf in self._attr_to_rest_field.values():
951 prop_meta = getattr(rf, "_xml", {})
952 xml_name = prop_meta.get("name", rf._rest_name)
953 xml_ns = _resolve_xml_ns(prop_meta, model_meta)
954 if xml_ns:
955 xml_name = "{" + xml_ns + "}" + xml_name
956
957 # attribute
958 if prop_meta.get("attribute", False) and element.get(xml_name) is not None:
959 existed_attr_keys.append(xml_name)
960 result[rf._rest_name] = _deserialize(rf._type, element.get(xml_name))
961 continue
962
963 # unwrapped element is array
964 if prop_meta.get("unwrapped", False):
965 _items_name = prop_meta.get("itemsName")
966 if _items_name:
967 xml_name = _items_name
968 _items_ns = prop_meta.get("itemsNs")
969 if _items_ns is not None:
970 xml_ns = _items_ns
971 if xml_ns:
972 xml_name = "{" + xml_ns + "}" + xml_name
973 items = element.findall(xml_name) # pyright: ignore
974 if len(items) > 0:
975 existed_attr_keys.append(xml_name)
976 result[rf._rest_name] = _deserialize(rf._type, items)
977 elif not rf._is_optional:
978 existed_attr_keys.append(xml_name)
979 result[rf._rest_name] = []
980 continue
981
982 # text element is primitive type
983 if prop_meta.get("text", False):
984 if element.text is not None:
985 result[rf._rest_name] = _deserialize(rf._type, element.text)
986 continue
987
988 # wrapped element could be normal property or array
989 item = element.find(xml_name)
990 if item is not None:
991 existed_attr_keys.append(xml_name)
992 result[rf._rest_name] = _deserialize(rf._type, item)
993
994 # rest thing is additional properties
995 for e in element:
996 if e.tag not in existed_attr_keys:
997 result[e.tag] = _convert_element(e)
998
999 return result
1000
1001 def copy(self) -> "Model":
1002 return Model(self.__dict__)
1003
1004 def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self:
1005 if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated:
1006 # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping',
1007 # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object'
1008 mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order
1009 attr_to_rest_field: dict[str, _RestField] = { # map attribute name to rest_field property
1010 k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type")
1011 }
1012 annotations = {
1013 k: v
1014 for mro_class in mros
1015 if hasattr(mro_class, "__annotations__")
1016 for k, v in mro_class.__annotations__.items()
1017 }
1018 for attr, rf in attr_to_rest_field.items():
1019 rf._module = cls.__module__
1020 if not rf._type:
1021 rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None))
1022 if not rf._rest_name_input:
1023 rf._rest_name_input = attr
1024 cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items())
1025 # Build XML field plan for fast _init_from_xml (only for XML models)
1026 if getattr(cls, "_xml", None):
1027 cls._xml_field_plan = _build_xml_field_plan(cls, attr_to_rest_field)
1028 cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
1029
1030 return super().__new__(cls)
1031
1032 def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None:
1033 for base in cls.__bases__:
1034 if hasattr(base, "__mapping__"):
1035 base.__mapping__[discriminator or cls.__name__] = cls # type: ignore
1036
1037 @classmethod
1038 def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]:
1039 for v in cls.__dict__.values():
1040 if isinstance(v, _RestField) and v._is_discriminator and v._rest_name not in exist_discriminators:
1041 return v
1042 return None
1043
1044 @classmethod
1045 def _deserialize(cls, data, exist_discriminators):
1046 if not hasattr(cls, "__mapping__"):
1047 return cls(data)
1048 discriminator = cls._get_discriminator(exist_discriminators)
1049 if discriminator is None:
1050 return cls(data)
1051 exist_discriminators.append(discriminator._rest_name)
1052 if isinstance(data, ET.Element):
1053 model_meta = getattr(cls, "_xml", {})
1054 prop_meta = getattr(discriminator, "_xml", {})
1055 xml_name = prop_meta.get("name", discriminator._rest_name)
1056 xml_ns = _resolve_xml_ns(prop_meta, model_meta)
1057 if xml_ns:
1058 xml_name = "{" + xml_ns + "}" + xml_name
1059
1060 if data.get(xml_name) is not None:
1061 discriminator_value = data.get(xml_name)
1062 else:
1063 discriminator_value = data.find(xml_name).text # pyright: ignore
1064 else:
1065 discriminator_value = data.get(discriminator._rest_name)
1066 mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore # pylint: disable=no-member
1067 return mapped_cls._deserialize(data, exist_discriminators)
1068
1069 def as_dict(self, *, exclude_readonly: bool = False) -> dict[str, typing.Any]:
1070 """Return a dict that can be turned into json using json.dump.
1071
1072 :keyword bool exclude_readonly: Whether to remove the readonly properties.
1073 :returns: A dict JSON compatible object
1074 :rtype: dict
1075 """
1076
1077 result = {}
1078 readonly_props = []
1079 if exclude_readonly:
1080 readonly_props = [p._rest_name for p in self._attr_to_rest_field.values() if _is_readonly(p)]
1081 for k, v in self.items():
1082 if exclude_readonly and k in readonly_props: # pyright: ignore
1083 continue
1084 is_multipart_file_input = False
1085 try:
1086 is_multipart_file_input = next(
1087 rf for rf in self._attr_to_rest_field.values() if rf._rest_name == k
1088 )._is_multipart_file_input
1089 except StopIteration:
1090 pass
1091 result[k] = v if is_multipart_file_input else Model._as_dict_value(v, exclude_readonly=exclude_readonly)
1092 return result
1093
1094 @staticmethod
1095 def _as_dict_value(v: typing.Any, exclude_readonly: bool = False) -> typing.Any:
1096 if v is None or isinstance(v, _Null):
1097 return None
1098 if isinstance(v, (list, tuple, set)):
1099 return type(v)(Model._as_dict_value(x, exclude_readonly=exclude_readonly) for x in v)
1100 if isinstance(v, dict):
1101 return {dk: Model._as_dict_value(dv, exclude_readonly=exclude_readonly) for dk, dv in v.items()}
1102 return v.as_dict(exclude_readonly=exclude_readonly) if hasattr(v, "as_dict") else v
1103
1104
1105def _deserialize_model(model_deserializer: typing.Optional[typing.Callable], obj):
1106 if _is_model(obj):
1107 return obj
1108 return _deserialize(model_deserializer, obj)
1109
1110
1111def _deserialize_with_optional(if_obj_deserializer: typing.Optional[typing.Callable], obj):
1112 if obj is None:
1113 return obj
1114 return _deserialize_with_callable(if_obj_deserializer, obj)
1115
1116
1117def _deserialize_with_union(deserializers, obj):
1118 for deserializer in deserializers:
1119 try:
1120 return _deserialize(deserializer, obj)
1121 except DeserializationError:
1122 pass
1123 raise DeserializationError()
1124
1125
1126def _deserialize_dict(
1127 value_deserializer: typing.Optional[typing.Callable],
1128 module: typing.Optional[str],
1129 obj: dict[typing.Any, typing.Any],
1130):
1131 if obj is None:
1132 return obj
1133 if isinstance(obj, ET.Element):
1134 obj = {child.tag: child for child in obj}
1135 return {k: _deserialize(value_deserializer, v, module) for k, v in obj.items()}
1136
1137
1138def _deserialize_multiple_sequence(
1139 entry_deserializers: list[typing.Optional[typing.Callable]],
1140 module: typing.Optional[str],
1141 obj,
1142):
1143 if obj is None:
1144 return obj
1145 return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers))
1146
1147
1148def _is_array_encoded_deserializer(deserializer: functools.partial) -> bool:
1149 return (
1150 isinstance(deserializer, functools.partial)
1151 and isinstance(deserializer.args[0], functools.partial)
1152 and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
1153 )
1154
1155
1156def _deserialize_sequence(
1157 deserializer: typing.Optional[typing.Callable],
1158 module: typing.Optional[str],
1159 obj,
1160):
1161 if obj is None:
1162 return obj
1163 if isinstance(obj, ET.Element):
1164 obj = list(obj)
1165
1166 # encoded string may be deserialized to sequence
1167 if isinstance(obj, str) and isinstance(deserializer, functools.partial):
1168 # for list[str]
1169 if _is_array_encoded_deserializer(deserializer):
1170 return deserializer(obj)
1171
1172 # for list[Union[...]]
1173 if isinstance(deserializer.args[0], list):
1174 for sub_deserializer in deserializer.args[0]:
1175 if _is_array_encoded_deserializer(sub_deserializer):
1176 return sub_deserializer(obj)
1177
1178 return type(obj)(_deserialize(deserializer, entry, module) for entry in obj)
1179
1180
1181def _sorted_annotations(types: list[typing.Any]) -> list[typing.Any]:
1182 return sorted(
1183 types,
1184 key=lambda x: hasattr(x, "__name__") and x.__name__.lower() in ("str", "float", "int", "bool"),
1185 )
1186
1187
1188def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-statements, too-many-branches
1189 annotation: typing.Any,
1190 module: typing.Optional[str],
1191 rf: typing.Optional["_RestField"] = None,
1192) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]:
1193 if not annotation:
1194 return None
1195
1196 # is it a type alias?
1197 if isinstance(annotation, str):
1198 if module is not None:
1199 annotation = _get_type_alias_type(module, annotation)
1200
1201 # is it a forward ref / in quotes?
1202 if isinstance(annotation, (str, typing.ForwardRef)):
1203 try:
1204 model_name = annotation.__forward_arg__ # type: ignore
1205 except AttributeError:
1206 model_name = annotation
1207 if module is not None:
1208 annotation = _get_model(module, model_name) # type: ignore
1209
1210 try:
1211 if module and _is_model(annotation):
1212 if rf:
1213 rf._is_model = True
1214
1215 return functools.partial(_deserialize_model, annotation) # pyright: ignore
1216 except Exception:
1217 pass
1218
1219 # is it a literal?
1220 try:
1221 if annotation.__origin__ is typing.Literal: # pyright: ignore
1222 return None
1223 except AttributeError:
1224 pass
1225
1226 # is it optional?
1227 try:
1228 if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore
1229 if rf:
1230 rf._is_optional = True
1231 if len(annotation.__args__) <= 2: # pyright: ignore
1232 if_obj_deserializer = _get_deserialize_callable_from_annotation(
1233 next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore
1234 )
1235
1236 return functools.partial(_deserialize_with_optional, if_obj_deserializer)
1237 # the type is Optional[Union[...]], we need to remove the None type from the Union
1238 annotation_copy = copy.copy(annotation)
1239 annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a is not _NONE_TYPE] # pyright: ignore
1240 return _get_deserialize_callable_from_annotation(annotation_copy, module, rf)
1241 except AttributeError:
1242 pass
1243
1244 # is it union?
1245 if getattr(annotation, "__origin__", None) is typing.Union:
1246 # initial ordering is we make `string` the last deserialization option, because it is often them most generic
1247 deserializers = [
1248 _get_deserialize_callable_from_annotation(arg, module, rf)
1249 for arg in _sorted_annotations(annotation.__args__) # pyright: ignore
1250 ]
1251
1252 return functools.partial(_deserialize_with_union, deserializers)
1253
1254 try:
1255 annotation_name = (
1256 annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore
1257 )
1258 if annotation_name.lower() == "dict":
1259 value_deserializer = _get_deserialize_callable_from_annotation(
1260 annotation.__args__[1], module, rf # pyright: ignore
1261 )
1262
1263 return functools.partial(
1264 _deserialize_dict,
1265 value_deserializer,
1266 module,
1267 )
1268 except (AttributeError, IndexError):
1269 pass
1270 try:
1271 annotation_name = (
1272 annotation.__name__ if hasattr(annotation, "__name__") else annotation._name # pyright: ignore
1273 )
1274 if annotation_name.lower() in ["list", "set", "tuple", "sequence"]:
1275 if len(annotation.__args__) > 1: # pyright: ignore
1276 entry_deserializers = [
1277 _get_deserialize_callable_from_annotation(dt, module, rf)
1278 for dt in annotation.__args__ # pyright: ignore
1279 ]
1280 return functools.partial(_deserialize_multiple_sequence, entry_deserializers, module)
1281 deserializer = _get_deserialize_callable_from_annotation(
1282 annotation.__args__[0], module, rf # pyright: ignore
1283 )
1284
1285 return functools.partial(_deserialize_sequence, deserializer, module)
1286 except (TypeError, IndexError, AttributeError, SyntaxError):
1287 pass
1288
1289 def _deserialize_default(
1290 deserializer,
1291 obj,
1292 ):
1293 if obj is None:
1294 return obj
1295 try:
1296 return _deserialize_with_callable(deserializer, obj)
1297 except Exception:
1298 pass
1299 return obj
1300
1301 if get_deserializer(annotation, rf):
1302 return functools.partial(_deserialize_default, get_deserializer(annotation, rf))
1303
1304 return functools.partial(_deserialize_default, annotation)
1305
1306
1307def _deserialize_with_callable(
1308 deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]],
1309 value: typing.Any,
1310): # pylint: disable=too-many-return-statements
1311 try:
1312 if value is None or isinstance(value, _Null):
1313 return None
1314 if isinstance(value, ET.Element):
1315 if deserializer is str:
1316 return value.text or ""
1317 if deserializer is int:
1318 return int(value.text) if value.text else None
1319 if deserializer is float:
1320 return float(value.text) if value.text else None
1321 if deserializer is bool:
1322 return value.text == "true" if value.text else None
1323 if deserializer and deserializer in _DESERIALIZE_MAPPING.values():
1324 return deserializer(value.text) if value.text else None
1325 if deserializer and deserializer in _DESERIALIZE_MAPPING_WITHFORMAT.values():
1326 return deserializer(value.text) if value.text else None
1327 if deserializer is None:
1328 return value
1329 if deserializer in [int, float, bool]:
1330 return deserializer(value)
1331 if isinstance(deserializer, CaseInsensitiveEnumMeta):
1332 try:
1333 return deserializer(value.text if isinstance(value, ET.Element) else value)
1334 except ValueError:
1335 # for unknown value, return raw value
1336 return value.text if isinstance(value, ET.Element) else value
1337 if isinstance(deserializer, type) and issubclass(deserializer, Model):
1338 return deserializer._deserialize(value, [])
1339 return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value)
1340 except Exception as e:
1341 raise DeserializationError() from e
1342
1343
1344def _deserialize(
1345 deserializer: typing.Any,
1346 value: typing.Any,
1347 module: typing.Optional[str] = None,
1348 rf: typing.Optional["_RestField"] = None,
1349 format: typing.Optional[str] = None,
1350) -> typing.Any:
1351 if isinstance(value, PipelineResponse):
1352 value = value.http_response.json()
1353 if rf is None and format:
1354 rf = _RestField(format=format)
1355 if not isinstance(deserializer, functools.partial):
1356 deserializer = _get_deserialize_callable_from_annotation(deserializer, module, rf)
1357 return _deserialize_with_callable(deserializer, value)
1358
1359
1360def _failsafe_deserialize(
1361 deserializer: typing.Any,
1362 response: HttpResponse,
1363 module: typing.Optional[str] = None,
1364 rf: typing.Optional["_RestField"] = None,
1365 format: typing.Optional[str] = None,
1366) -> typing.Any:
1367 try:
1368 return _deserialize(deserializer, response.json(), module, rf, format)
1369 except Exception: # pylint: disable=broad-except
1370 _LOGGER.warning(
1371 "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True
1372 )
1373 return None
1374
1375
1376def _failsafe_deserialize_xml(
1377 deserializer: typing.Any,
1378 response: HttpResponse,
1379) -> typing.Any:
1380 try:
1381 return _deserialize_xml(deserializer, response.text())
1382 except Exception: # pylint: disable=broad-except
1383 _LOGGER.warning(
1384 "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True
1385 )
1386 return None
1387
1388
1389# pylint: disable=too-many-instance-attributes
1390class _RestField:
1391 def __init__(
1392 self,
1393 *,
1394 name: typing.Optional[str] = None,
1395 type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin
1396 is_discriminator: bool = False,
1397 visibility: typing.Optional[list[str]] = None,
1398 default: typing.Any = _UNSET,
1399 format: typing.Optional[str] = None,
1400 is_multipart_file_input: bool = False,
1401 xml: typing.Optional[dict[str, typing.Any]] = None,
1402 deserializer: typing.Optional[typing.Callable] = None,
1403 ):
1404 self._type = type
1405 self._rest_name_input = name
1406 self._module: typing.Optional[str] = None
1407 self._is_discriminator = is_discriminator
1408 self._visibility = visibility
1409 self._is_model = False
1410 self._is_optional = False
1411 self._default = default
1412 self._format = format
1413 self._is_multipart_file_input = is_multipart_file_input
1414 self._xml = xml if xml is not None else {}
1415 self._deserializer = deserializer
1416
1417 @property
1418 def _class_type(self) -> typing.Any:
1419 result = getattr(self._type, "args", [None])[0]
1420 # type may be wrapped by nested functools.partial so we need to check for that
1421 if isinstance(result, functools.partial):
1422 return getattr(result, "args", [None])[0]
1423 return result
1424
1425 @property
1426 def _rest_name(self) -> str:
1427 if self._rest_name_input is None:
1428 raise ValueError("Rest name was never set")
1429 return self._rest_name_input
1430
1431 def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin
1432 # by this point, type and rest_name will have a value bc we default
1433 # them in __new__ of the Model class
1434 # Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1435 item = obj._data.get(self._rest_name, _UNSET)
1436 if item is _UNSET:
1437 # Field not set by user; return the client default if one exists, otherwise None
1438 return self._default if self._default is not _UNSET else None
1439 if item is None:
1440 return item
1441 if self._is_model:
1442 return item
1443
1444 # For mutable types, we want mutations to directly affect _data
1445 # Check if we've already deserialized this value
1446 cache_attr = f"_deserialized_{self._rest_name}"
1447 if hasattr(obj, cache_attr):
1448 # Return the value from _data directly (it's been deserialized in place)
1449 return obj._data.get(self._rest_name)
1450
1451 # Fast path: use _deserializer directly (avoids _serialize/_deserialize chain)
1452 if self._deserializer:
1453 deserialized = self._deserializer(item)
1454 else:
1455 deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1456
1457 # For mutable types, store the deserialized value back in _data
1458 # so mutations directly affect _data
1459 if isinstance(deserialized, (dict, list, set)):
1460 obj._data[self._rest_name] = deserialized
1461 object.__setattr__(obj, cache_attr, True) # Mark as deserialized
1462 return deserialized
1463
1464 return deserialized
1465
1466 def __set__(self, obj: Model, value) -> None:
1467 # Clear the cached deserialized object when setting a new value
1468 cache_attr = f"_deserialized_{self._rest_name}"
1469 if hasattr(obj, cache_attr):
1470 object.__delattr__(obj, cache_attr)
1471
1472 if value is None:
1473 # we want to wipe out entries if users set attr to None
1474 try:
1475 obj.__delitem__(self._rest_name)
1476 except KeyError:
1477 pass
1478 return
1479 if self._is_model:
1480 if not _is_model(value):
1481 value = _deserialize(self._type, value)
1482 obj.__setitem__(self._rest_name, value)
1483 return
1484 obj.__setitem__(self._rest_name, _serialize(value, self._format))
1485
1486 def _get_deserialize_callable_from_annotation(
1487 self, annotation: typing.Any
1488 ) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]:
1489 return _get_deserialize_callable_from_annotation(annotation, self._module, self)
1490
1491
1492def rest_field(
1493 *,
1494 name: typing.Optional[str] = None,
1495 type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin
1496 visibility: typing.Optional[list[str]] = None,
1497 default: typing.Any = _UNSET,
1498 format: typing.Optional[str] = None,
1499 is_multipart_file_input: bool = False,
1500 xml: typing.Optional[dict[str, typing.Any]] = None,
1501 deserializer: typing.Optional[typing.Callable] = None,
1502) -> typing.Any:
1503 return _RestField(
1504 name=name,
1505 type=type,
1506 visibility=visibility,
1507 default=default,
1508 format=format,
1509 is_multipart_file_input=is_multipart_file_input,
1510 xml=xml,
1511 deserializer=deserializer,
1512 )
1513
1514
1515def rest_discriminator(
1516 *,
1517 name: typing.Optional[str] = None,
1518 type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin
1519 visibility: typing.Optional[list[str]] = None,
1520 xml: typing.Optional[dict[str, typing.Any]] = None,
1521) -> typing.Any:
1522 return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml)
1523
1524
1525def serialize_xml(model: Model, exclude_readonly: bool = False) -> str:
1526 """Serialize a model to XML.
1527
1528 :param Model model: The model to serialize.
1529 :param bool exclude_readonly: Whether to exclude readonly properties.
1530 :returns: The XML representation of the model.
1531 :rtype: str
1532 """
1533 return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore
1534
1535
1536def _get_xml_ns(meta: dict[str, typing.Any]) -> typing.Optional[str]:
1537 """Return the XML namespace from a metadata dict, checking both 'ns' (old-style) and 'namespace' (DPG) keys.
1538
1539 :param dict meta: The metadata dictionary to extract namespace from.
1540 :returns: The namespace string if 'ns' or 'namespace' key is present, None otherwise.
1541 :rtype: str or None
1542 """
1543 ns = meta.get("ns")
1544 if ns is None:
1545 ns = meta.get("namespace")
1546 return ns
1547
1548
1549def _resolve_xml_ns(
1550 prop_meta: dict[str, typing.Any], model_meta: typing.Optional[dict[str, typing.Any]] = None
1551) -> typing.Optional[str]:
1552 """Resolve XML namespace for a property, falling back to model namespace when appropriate.
1553
1554 Checks the property metadata first; if no namespace is found and the model does not declare
1555 an explicit prefix, falls back to the model-level namespace.
1556
1557 :param dict prop_meta: The property metadata dictionary.
1558 :param dict model_meta: The model metadata dictionary, used as fallback.
1559 :returns: The resolved namespace string, or None.
1560 :rtype: str or None
1561 """
1562 ns = _get_xml_ns(prop_meta)
1563 if ns is None and model_meta is not None and not model_meta.get("prefix"):
1564 ns = _get_xml_ns(model_meta)
1565 return ns
1566
1567
1568def _set_xml_attribute(element: ET.Element, name: str, value: typing.Any, prop_meta: dict[str, typing.Any]) -> None:
1569 """Set an XML attribute on an element, handling namespace prefix registration.
1570
1571 :param ET.Element element: The element to set the attribute on.
1572 :param str name: The default attribute name (wire name).
1573 :param any value: The attribute value.
1574 :param dict prop_meta: The property metadata dictionary.
1575 """
1576 xml_name = prop_meta.get("name", name)
1577 _attr_ns = _get_xml_ns(prop_meta)
1578 if _attr_ns:
1579 _attr_prefix = prop_meta.get("prefix")
1580 if _attr_prefix:
1581 _safe_register_namespace(_attr_prefix, _attr_ns)
1582 xml_name = "{" + _attr_ns + "}" + xml_name
1583 element.set(xml_name, _get_primitive_type_value(value))
1584
1585
1586def _get_element(
1587 o: typing.Any,
1588 exclude_readonly: bool = False,
1589 parent_meta: typing.Optional[dict[str, typing.Any]] = None,
1590 wrapped_element: typing.Optional[ET.Element] = None,
1591) -> typing.Union[ET.Element, list[ET.Element]]:
1592 if _is_model(o):
1593 model_meta = getattr(o, "_xml", {})
1594
1595 # if prop is a model, then use the prop element directly, else generate a wrapper of model
1596 if wrapped_element is None:
1597 # When serializing as an array item (parent_meta is set), check if the parent has an
1598 # explicit itemsName. This ensures correct element names for unwrapped arrays (where
1599 # the element tag is the property/items name, not the model type name).
1600 _items_name = parent_meta.get("itemsName") if parent_meta is not None else None
1601 element_name = _items_name if _items_name else (model_meta.get("name") or o.__class__.__name__)
1602 _model_ns = _get_xml_ns(model_meta)
1603 wrapped_element = _create_xml_element(
1604 element_name,
1605 model_meta.get("prefix"),
1606 _model_ns,
1607 )
1608
1609 readonly_props = []
1610 if exclude_readonly:
1611 readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)]
1612
1613 for k, v in o.items():
1614 # do not serialize readonly properties
1615 if exclude_readonly and k in readonly_props:
1616 continue
1617
1618 prop_rest_field = _get_rest_field(o._attr_to_rest_field, k)
1619 if prop_rest_field:
1620 prop_meta = getattr(prop_rest_field, "_xml").copy()
1621 # use the wire name as xml name if no specific name is set
1622 if prop_meta.get("name") is None:
1623 prop_meta["name"] = k
1624 else:
1625 # additional properties will not have rest field, use the wire name as xml name
1626 prop_meta = {"name": k}
1627
1628 # Propagate model namespace to properties only for old-style "ns"-keyed models.
1629 # DPG-generated models use the "namespace" key and explicitly declare namespace on
1630 # each property that needs it, so propagation is intentionally skipped for them.
1631 if prop_meta.get("ns") is None and model_meta.get("ns"):
1632 prop_meta["ns"] = model_meta.get("ns")
1633 prop_meta["prefix"] = model_meta.get("prefix")
1634
1635 if prop_meta.get("unwrapped", False):
1636 # unwrapped could only set on array
1637 wrapped_element.extend(_get_element(v, exclude_readonly, prop_meta))
1638 elif prop_meta.get("text", False):
1639 # text could only set on primitive type
1640 wrapped_element.text = _get_primitive_type_value(v)
1641 elif prop_meta.get("attribute", False):
1642 _set_xml_attribute(wrapped_element, k, v, prop_meta)
1643 else:
1644 # other wrapped prop element
1645 wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta))
1646 return wrapped_element
1647 if isinstance(o, list):
1648 return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore
1649 if isinstance(o, dict):
1650 result = []
1651 _dict_ns = _get_xml_ns(parent_meta) if parent_meta else None
1652 for k, v in o.items():
1653 result.append(
1654 _get_wrapped_element(
1655 v,
1656 exclude_readonly,
1657 {
1658 "name": k,
1659 "ns": _dict_ns,
1660 "prefix": parent_meta.get("prefix") if parent_meta else None,
1661 },
1662 )
1663 )
1664 return result
1665
1666 # primitive case need to create element based on parent_meta
1667 if parent_meta:
1668 _items_ns = parent_meta.get("itemsNs")
1669 if _items_ns is None:
1670 _items_ns = _get_xml_ns(parent_meta)
1671 return _get_wrapped_element(
1672 o,
1673 exclude_readonly,
1674 {
1675 "name": parent_meta.get("itemsName", parent_meta.get("name")),
1676 "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")),
1677 "ns": _items_ns,
1678 },
1679 )
1680
1681 raise ValueError("Could not serialize value into xml: " + o)
1682
1683
1684def _get_wrapped_element(
1685 v: typing.Any,
1686 exclude_readonly: bool,
1687 meta: typing.Optional[dict[str, typing.Any]],
1688) -> ET.Element:
1689 _meta_ns = _get_xml_ns(meta) if meta else None
1690 wrapped_element = _create_xml_element(
1691 meta.get("name") if meta else None, meta.get("prefix") if meta else None, _meta_ns
1692 )
1693 if isinstance(v, (dict, list)):
1694 wrapped_element.extend(_get_element(v, exclude_readonly, meta))
1695 elif _is_model(v):
1696 _get_element(v, exclude_readonly, meta, wrapped_element)
1697 else:
1698 wrapped_element.text = _get_primitive_type_value(v)
1699 return wrapped_element # type: ignore[no-any-return]
1700
1701
1702def _get_primitive_type_value(v) -> str:
1703 if v is True:
1704 return "true"
1705 if v is False:
1706 return "false"
1707 if isinstance(v, _Null):
1708 return ""
1709 return str(v)
1710
1711
1712def _safe_register_namespace(prefix: str, ns: str) -> None:
1713 """Register an XML namespace prefix, handling reserved prefix patterns.
1714
1715 Some prefixes (e.g. 'ns2') match Python's reserved 'ns\\d+' pattern used for
1716 auto-generated prefixes, causing register_namespace to raise ValueError.
1717 Falls back to directly registering in the internal namespace map.
1718
1719 :param str prefix: The namespace prefix to register.
1720 :param str ns: The namespace URI.
1721 """
1722 try:
1723 ET.register_namespace(prefix, ns)
1724 except ValueError:
1725 _ns_map = getattr(ET, "_namespace_map", None)
1726 if _ns_map is not None:
1727 _ns_map[ns] = prefix
1728
1729
1730def _create_xml_element(
1731 tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None
1732) -> ET.Element:
1733 if prefix and ns:
1734 _safe_register_namespace(prefix, ns)
1735 if ns:
1736 return ET.Element("{" + ns + "}" + tag)
1737 return ET.Element(tag)
1738
1739
1740def _deserialize_xml(
1741 deserializer: typing.Any,
1742 value: str,
1743) -> typing.Any:
1744 element = ET.fromstring(value) # nosec
1745 if _is_model(deserializer):
1746 return deserializer._deserialize(element, [])
1747 return _deserialize(deserializer, element)
1748
1749
1750def _convert_element(e: ET.Element):
1751 # dict case
1752 if len(e.attrib) > 0 or len({child.tag for child in e}) > 1:
1753 dict_result: dict[str, typing.Any] = {}
1754 for child in e:
1755 if dict_result.get(child.tag) is not None:
1756 if isinstance(dict_result[child.tag], list):
1757 dict_result[child.tag].append(_convert_element(child))
1758 else:
1759 dict_result[child.tag] = [dict_result[child.tag], _convert_element(child)]
1760 else:
1761 dict_result[child.tag] = _convert_element(child)
1762 dict_result.update(e.attrib)
1763 return dict_result
1764 # array case
1765 if len(e) > 0:
1766 array_result: list[typing.Any] = []
1767 for child in e:
1768 array_result.append(_convert_element(child))
1769 return array_result
1770 # primitive case
1771 return e.text