Skip to content

Commit 7337d70

Browse files
cRui861Rena Chen
andauthored
[Batch] Beta data plane SDK v15.1.0b3 Track 2 (#44681)
* Initial generation and version update to remove beta * Update tests * Fix sphinx issues and update changelog * Fix CI pipeline errors * Update classifier * Revert back to beta - will switch entire PR to a beta PR now * Re-generate for storage_account_type removal --------- Co-authored-by: Rena Chen <rechen@microsoft.com>
1 parent 65612ca commit 7337d70

File tree

11 files changed

+229
-91
lines changed

11 files changed

+229
-91
lines changed

sdk/batch/azure-batch/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release History
22

3+
## 15.1.0b3 (2026-02-05)
4+
5+
### Other Changes
6+
7+
- Minor parameter renaming: `read_io_gi_b` to `read_io_gib`, `write_io_gi_b` to `write_io_gib`, and `v_tpm_enabled` to `vtpm_enabled`.
8+
39
## 15.1.0b2 (2025-11-20)
410

511
### Features Added

sdk/batch/azure-batch/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/batch/azure-batch",
5-
"Tag": "python/batch/azure-batch_3e56fd21d3"
5+
"Tag": "python/batch/azure-batch_2f5749e81e"
66
}

sdk/batch/azure-batch/azure/batch/_serialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ def serialize_data(self, data, data_type, **kwargs):
808808
# If dependencies is empty, try with current data class
809809
# It has to be a subclass of Enum anyway
810810
enum_type = self.dependencies.get(data_type, data.__class__)
811-
if issubclass(enum_type, Enum):
811+
if issubclass(enum_type, Enum): # type: ignore[arg-type]
812812
return Serializer.serialize_enum(data, enum_obj=enum_type)
813813

814814
iter_type = data_type[0] + data_type[-1]

sdk/batch/azure-batch/azure/batch/_utils/model_base.py

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
TZ_UTC = timezone.utc
3939
_T = typing.TypeVar("_T")
40+
_NONE_TYPE = type(None)
4041

4142

4243
def _timedelta_as_isostr(td: timedelta) -> str:
@@ -171,6 +172,21 @@ def default(self, o): # pylint: disable=too-many-return-statements
171172
r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT"
172173
)
173174

175+
_ARRAY_ENCODE_MAPPING = {
176+
"pipeDelimited": "|",
177+
"spaceDelimited": " ",
178+
"commaDelimited": ",",
179+
"newlineDelimited": "\n",
180+
}
181+
182+
183+
def _deserialize_array_encoded(delimit: str, attr):
184+
if isinstance(attr, str):
185+
if attr == "":
186+
return []
187+
return attr.split(delimit)
188+
return attr
189+
174190

175191
def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
176192
"""Deserialize ISO-8601 formatted string into Datetime object.
@@ -202,7 +218,7 @@ def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
202218
test_utc = date_obj.utctimetuple()
203219
if test_utc.tm_year > 9999 or test_utc.tm_year < 1:
204220
raise OverflowError("Hit max or min date")
205-
return date_obj
221+
return date_obj # type: ignore[no-any-return]
206222

207223

208224
def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime:
@@ -256,7 +272,7 @@ def _deserialize_time(attr: typing.Union[str, time]) -> time:
256272
"""
257273
if isinstance(attr, time):
258274
return attr
259-
return isodate.parse_time(attr)
275+
return isodate.parse_time(attr) # type: ignore[no-any-return]
260276

261277

262278
def _deserialize_bytes(attr):
@@ -315,6 +331,8 @@ def _deserialize_int_as_str(attr):
315331
def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None):
316332
if annotation is int and rf and rf._format == "str":
317333
return _deserialize_int_as_str
334+
if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING:
335+
return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format])
318336
if rf and rf._format:
319337
return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format)
320338
return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore
@@ -353,9 +371,39 @@ def __contains__(self, key: typing.Any) -> bool:
353371
return key in self._data
354372

355373
def __getitem__(self, key: str) -> typing.Any:
374+
# If this key has been deserialized (for mutable types), we need to handle serialization
375+
if hasattr(self, "_attr_to_rest_field"):
376+
cache_attr = f"_deserialized_{key}"
377+
if hasattr(self, cache_attr):
378+
rf = _get_rest_field(getattr(self, "_attr_to_rest_field"), key)
379+
if rf:
380+
value = self._data.get(key)
381+
if isinstance(value, (dict, list, set)):
382+
# For mutable types, serialize and return
383+
# But also update _data with serialized form and clear flag
384+
# so mutations via this returned value affect _data
385+
serialized = _serialize(value, rf._format)
386+
# If serialized form is same type (no transformation needed),
387+
# return _data directly so mutations work
388+
if isinstance(serialized, type(value)) and serialized == value:
389+
return self._data.get(key)
390+
# Otherwise return serialized copy and clear flag
391+
try:
392+
object.__delattr__(self, cache_attr)
393+
except AttributeError:
394+
pass
395+
# Store serialized form back
396+
self._data[key] = serialized
397+
return serialized
356398
return self._data.__getitem__(key)
357399

358400
def __setitem__(self, key: str, value: typing.Any) -> None:
401+
# Clear any cached deserialized value when setting through dictionary access
402+
cache_attr = f"_deserialized_{key}"
403+
try:
404+
object.__delattr__(self, cache_attr)
405+
except AttributeError:
406+
pass
359407
self._data.__setitem__(key, value)
360408

361409
def __delitem__(self, key: str) -> None:
@@ -483,6 +531,8 @@ def _is_model(obj: typing.Any) -> bool:
483531

484532
def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements
485533
if isinstance(o, list):
534+
if format in _ARRAY_ENCODE_MAPPING and all(isinstance(x, str) for x in o):
535+
return _ARRAY_ENCODE_MAPPING[format].join(o)
486536
return [_serialize(x, format) for x in o]
487537
if isinstance(o, dict):
488538
return {k: _serialize(v, format) for k, v in o.items()}
@@ -638,6 +688,10 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self:
638688
if not rf._rest_name_input:
639689
rf._rest_name_input = attr
640690
cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items())
691+
cls._backcompat_attr_to_rest_field: dict[str, _RestField] = {
692+
Model._get_backcompat_attribute_name(cls._attr_to_rest_field, attr): rf
693+
for attr, rf in cls._attr_to_rest_field.items()
694+
}
641695
cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
642696

643697
return super().__new__(cls)
@@ -647,6 +701,16 @@ def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None:
647701
if hasattr(base, "__mapping__"):
648702
base.__mapping__[discriminator or cls.__name__] = cls # type: ignore
649703

704+
@classmethod
705+
def _get_backcompat_attribute_name(cls, attr_to_rest_field: dict[str, "_RestField"], attr_name: str) -> str:
706+
rest_field_obj = attr_to_rest_field.get(attr_name) # pylint: disable=protected-access
707+
if rest_field_obj is None:
708+
return attr_name
709+
original_tsp_name = getattr(rest_field_obj, "_original_tsp_name", None) # pylint: disable=protected-access
710+
if original_tsp_name:
711+
return original_tsp_name
712+
return attr_name
713+
650714
@classmethod
651715
def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]:
652716
for v in cls.__dict__.values():
@@ -758,6 +822,14 @@ def _deserialize_multiple_sequence(
758822
return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers))
759823

760824

825+
def _is_array_encoded_deserializer(deserializer: functools.partial) -> bool:
826+
return (
827+
isinstance(deserializer, functools.partial)
828+
and isinstance(deserializer.args[0], functools.partial)
829+
and deserializer.args[0].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
830+
)
831+
832+
761833
def _deserialize_sequence(
762834
deserializer: typing.Optional[typing.Callable],
763835
module: typing.Optional[str],
@@ -767,6 +839,19 @@ def _deserialize_sequence(
767839
return obj
768840
if isinstance(obj, ET.Element):
769841
obj = list(obj)
842+
843+
# encoded string may be deserialized to sequence
844+
if isinstance(obj, str) and isinstance(deserializer, functools.partial):
845+
# for list[str]
846+
if _is_array_encoded_deserializer(deserializer):
847+
return deserializer(obj)
848+
849+
# for list[Union[...]]
850+
if isinstance(deserializer.args[0], list):
851+
for sub_deserializer in deserializer.args[0]:
852+
if _is_array_encoded_deserializer(sub_deserializer):
853+
return sub_deserializer(obj)
854+
770855
return type(obj)(_deserialize(deserializer, entry, module) for entry in obj)
771856

772857

@@ -817,16 +902,16 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur
817902

818903
# is it optional?
819904
try:
820-
if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore
905+
if any(a is _NONE_TYPE for a in annotation.__args__): # pyright: ignore
821906
if len(annotation.__args__) <= 2: # pyright: ignore
822907
if_obj_deserializer = _get_deserialize_callable_from_annotation(
823-
next(a for a in annotation.__args__ if a != type(None)), module, rf # pyright: ignore
908+
next(a for a in annotation.__args__ if a is not _NONE_TYPE), module, rf # pyright: ignore
824909
)
825910

826911
return functools.partial(_deserialize_with_optional, if_obj_deserializer)
827912
# the type is Optional[Union[...]], we need to remove the None type from the Union
828913
annotation_copy = copy.copy(annotation)
829-
annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a != type(None)] # pyright: ignore
914+
annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a is not _NONE_TYPE] # pyright: ignore
830915
return _get_deserialize_callable_from_annotation(annotation_copy, module, rf)
831916
except AttributeError:
832917
pass
@@ -972,6 +1057,7 @@ def _failsafe_deserialize_xml(
9721057
return None
9731058

9741059

1060+
# pylint: disable=too-many-instance-attributes
9751061
class _RestField:
9761062
def __init__(
9771063
self,
@@ -984,6 +1070,7 @@ def __init__(
9841070
format: typing.Optional[str] = None,
9851071
is_multipart_file_input: bool = False,
9861072
xml: typing.Optional[dict[str, typing.Any]] = None,
1073+
original_tsp_name: typing.Optional[str] = None,
9871074
):
9881075
self._type = type
9891076
self._rest_name_input = name
@@ -995,10 +1082,15 @@ def __init__(
9951082
self._format = format
9961083
self._is_multipart_file_input = is_multipart_file_input
9971084
self._xml = xml if xml is not None else {}
1085+
self._original_tsp_name = original_tsp_name
9981086

9991087
@property
10001088
def _class_type(self) -> typing.Any:
1001-
return getattr(self._type, "args", [None])[0]
1089+
result = getattr(self._type, "args", [None])[0]
1090+
# type may be wrapped by nested functools.partial so we need to check for that
1091+
if isinstance(result, functools.partial):
1092+
return getattr(result, "args", [None])[0]
1093+
return result
10021094

10031095
@property
10041096
def _rest_name(self) -> str:
@@ -1009,14 +1101,37 @@ def _rest_name(self) -> str:
10091101
def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin
10101102
# by this point, type and rest_name will have a value bc we default
10111103
# them in __new__ of the Model class
1012-
item = obj.get(self._rest_name)
1104+
# Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1105+
item = obj._data.get(self._rest_name)
10131106
if item is None:
10141107
return item
10151108
if self._is_model:
10161109
return item
1017-
return _deserialize(self._type, _serialize(item, self._format), rf=self)
1110+
1111+
# For mutable types, we want mutations to directly affect _data
1112+
# Check if we've already deserialized this value
1113+
cache_attr = f"_deserialized_{self._rest_name}"
1114+
if hasattr(obj, cache_attr):
1115+
# Return the value from _data directly (it's been deserialized in place)
1116+
return obj._data.get(self._rest_name)
1117+
1118+
deserialized = _deserialize(self._type, _serialize(item, self._format), rf=self)
1119+
1120+
# For mutable types, store the deserialized value back in _data
1121+
# so mutations directly affect _data
1122+
if isinstance(deserialized, (dict, list, set)):
1123+
obj._data[self._rest_name] = deserialized
1124+
object.__setattr__(obj, cache_attr, True) # Mark as deserialized
1125+
return deserialized
1126+
1127+
return deserialized
10181128

10191129
def __set__(self, obj: Model, value) -> None:
1130+
# Clear the cached deserialized object when setting a new value
1131+
cache_attr = f"_deserialized_{self._rest_name}"
1132+
if hasattr(obj, cache_attr):
1133+
object.__delattr__(obj, cache_attr)
1134+
10201135
if value is None:
10211136
# we want to wipe out entries if users set attr to None
10221137
try:
@@ -1046,6 +1161,7 @@ def rest_field(
10461161
format: typing.Optional[str] = None,
10471162
is_multipart_file_input: bool = False,
10481163
xml: typing.Optional[dict[str, typing.Any]] = None,
1164+
original_tsp_name: typing.Optional[str] = None,
10491165
) -> typing.Any:
10501166
return _RestField(
10511167
name=name,
@@ -1055,6 +1171,7 @@ def rest_field(
10551171
format=format,
10561172
is_multipart_file_input=is_multipart_file_input,
10571173
xml=xml,
1174+
original_tsp_name=original_tsp_name,
10581175
)
10591176

10601177

@@ -1184,7 +1301,7 @@ def _get_wrapped_element(
11841301
_get_element(v, exclude_readonly, meta, wrapped_element)
11851302
else:
11861303
wrapped_element.text = _get_primitive_type_value(v)
1187-
return wrapped_element
1304+
return wrapped_element # type: ignore[no-any-return]
11881305

11891306

11901307
def _get_primitive_type_value(v) -> str:
@@ -1197,7 +1314,9 @@ def _get_primitive_type_value(v) -> str:
11971314
return str(v)
11981315

11991316

1200-
def _create_xml_element(tag, prefix=None, ns=None):
1317+
def _create_xml_element(
1318+
tag: typing.Any, prefix: typing.Optional[str] = None, ns: typing.Optional[str] = None
1319+
) -> ET.Element:
12011320
if prefix and ns:
12021321
ET.register_namespace(prefix, ns)
12031322
if ns:

sdk/batch/azure-batch/azure/batch/_utils/serialization.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -821,13 +821,20 @@ def serialize_basic(cls, data, data_type, **kwargs):
821821
:param str data_type: Type of object in the iterable.
822822
:rtype: str, int, float, bool
823823
:return: serialized object
824+
:raises TypeError: raise if data_type is not one of str, int, float, bool.
824825
"""
825826
custom_serializer = cls._get_custom_serializers(data_type, **kwargs)
826827
if custom_serializer:
827828
return custom_serializer(data)
828829
if data_type == "str":
829830
return cls.serialize_unicode(data)
830-
return eval(data_type)(data) # nosec # pylint: disable=eval-used
831+
if data_type == "int":
832+
return int(data)
833+
if data_type == "float":
834+
return float(data)
835+
if data_type == "bool":
836+
return bool(data)
837+
raise TypeError("Unknown basic data type: {}".format(data_type))
831838

832839
@classmethod
833840
def serialize_unicode(cls, data):
@@ -1757,7 +1764,7 @@ def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return
17571764
:param str data_type: deserialization data type.
17581765
:return: Deserialized basic type.
17591766
:rtype: str, int, float or bool
1760-
:raises TypeError: if string format is not valid.
1767+
:raises TypeError: if string format is not valid or data_type is not one of str, int, float, bool.
17611768
"""
17621769
# If we're here, data is supposed to be a basic type.
17631770
# If it's still an XML node, take the text
@@ -1783,7 +1790,11 @@ def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return
17831790

17841791
if data_type == "str":
17851792
return self.deserialize_unicode(attr)
1786-
return eval(data_type)(attr) # nosec # pylint: disable=eval-used
1793+
if data_type == "int":
1794+
return int(attr)
1795+
if data_type == "float":
1796+
return float(attr)
1797+
raise TypeError("Unknown basic data type: {}".format(data_type))
17871798

17881799
@staticmethod
17891800
def deserialize_unicode(data):

sdk/batch/azure-batch/azure/batch/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
77
# --------------------------------------------------------------------------
88

9-
VERSION = "15.1.0b2"
9+
VERSION = "15.1.0b3"

0 commit comments

Comments
 (0)