Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-azure-core for
openSUSE:Factory checked in at 2026-01-06 17:41:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-azure-core (Old)
and /work/SRC/openSUSE:Factory/.python-azure-core.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-azure-core"
Tue Jan 6 17:41:35 2026 rev:58 rq:1325405 version:1.37.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-azure-core/python-azure-core.changes
2025-10-18 14:35:03.231488684 +0200
+++
/work/SRC/openSUSE:Factory/.python-azure-core.new.1928/python-azure-core.changes
2026-01-06 17:41:43.434601897 +0100
@@ -1,0 +2,8 @@
+Mon Jan 5 07:58:52 UTC 2026 - John Paul Adrian Glaubitz
<[email protected]>
+
+- New upstream release
+ + Version 1.37.0
+ + For detailed information about changes see the
+ CHANGELOG.md file provided with this package
+
+-------------------------------------------------------------------
Old:
----
azure_core-1.36.0.tar.gz
New:
----
azure_core-1.37.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-azure-core.spec ++++++
--- /var/tmp/diff_new_pack.Vn4r8J/_old 2026-01-06 17:41:44.082628486 +0100
+++ /var/tmp/diff_new_pack.Vn4r8J/_new 2026-01-06 17:41:44.082628486 +0100
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-azure-core
-Version: 1.36.0
+Version: 1.37.0
Release: 0
Summary: Microsoft Azure Core Library for Python
License: MIT
++++++ azure_core-1.36.0.tar.gz -> azure_core-1.37.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/CHANGELOG.md
new/azure_core-1.37.0/CHANGELOG.md
--- old/azure_core-1.36.0/CHANGELOG.md 2025-10-15 01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/CHANGELOG.md 2025-12-11 19:57:42.000000000 +0100
@@ -1,14 +1,24 @@
# Release History
+## 1.37.0 (2025-12-11)
+
+### Features Added
+
+- Added `get_backcompat_attr_name` to `azure.core.serialization`.
`get_backcompat_attr_name` gets the backcompat name of an attribute using
backcompat attribute access. #44084
+
## 1.36.0 (2025-10-14)
### Features Added
- Added `TypeHandlerRegistry` to `azure.core.serialization` to allow
developers to register custom serializers and deserializers for specific types
or conditions. #43051
+### Breaking Changes
+
### Bugs Fixed
- Fixed repeated import attempts of cchardet and chardet when
charset_normalizer is used #43092
+- Fixed leaked requests and aiohttp exceptions for streamed responses #43200
+- Improved granularity of ServiceRequestError and ServiceResponseError
exceptions raised in timeout scenarios from the requests and aiohttp transports
#43200
### Other Changes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/PKG-INFO
new/azure_core-1.37.0/PKG-INFO
--- old/azure_core-1.36.0/PKG-INFO 2025-10-15 01:34:50.758143200 +0200
+++ new/azure_core-1.37.0/PKG-INFO 2025-12-11 19:58:25.433900400 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: azure-core
-Version: 1.36.0
+Version: 1.37.0
Summary: Microsoft Azure Core Library for Python
Home-page:
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
Author: Microsoft Corporation
@@ -323,15 +323,25 @@
# Release History
+## 1.37.0 (2025-12-11)
+
+### Features Added
+
+- Added `get_backcompat_attr_name` to `azure.core.serialization`.
`get_backcompat_attr_name` gets the backcompat name of an attribute using
backcompat attribute access. #44084
+
## 1.36.0 (2025-10-14)
### Features Added
- Added `TypeHandlerRegistry` to `azure.core.serialization` to allow
developers to register custom serializers and deserializers for specific types
or conditions. #43051
+### Breaking Changes
+
### Bugs Fixed
- Fixed repeated import attempts of cchardet and chardet when
charset_normalizer is used #43092
+- Fixed leaked requests and aiohttp exceptions for streamed responses #43200
+- Improved granularity of ServiceRequestError and ServiceResponseError
exceptions raised in timeout scenarios from the requests and aiohttp transports
#43200
### Other Changes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/azure/core/_version.py
new/azure_core-1.37.0/azure/core/_version.py
--- old/azure_core-1.36.0/azure/core/_version.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/_version.py 2025-12-11
19:57:42.000000000 +0100
@@ -9,4 +9,4 @@
# regenerated.
# --------------------------------------------------------------------------
-VERSION = "1.36.0"
+VERSION = "1.37.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/azure/core/pipeline/_tools.py
new/azure_core-1.37.0/azure/core/pipeline/_tools.py
--- old/azure_core-1.36.0/azure/core/pipeline/_tools.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/pipeline/_tools.py 2025-12-11
19:57:42.000000000 +0100
@@ -80,7 +80,5 @@
"""
try:
response.read()
+ finally:
response.close()
- except Exception as exc:
- response.close()
- raise exc
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/azure/core/pipeline/_tools_async.py
new/azure_core-1.37.0/azure/core/pipeline/_tools_async.py
--- old/azure_core-1.36.0/azure/core/pipeline/_tools_async.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/pipeline/_tools_async.py 2025-12-11
19:57:42.000000000 +0100
@@ -67,7 +67,5 @@
"""
try:
await response.read()
+ finally:
await response.close()
- except Exception as exc:
- await response.close()
- raise exc
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/azure/core/pipeline/transport/_aiohttp.py
new/azure_core-1.37.0/azure/core/pipeline/transport/_aiohttp.py
--- old/azure_core-1.36.0/azure/core/pipeline/transport/_aiohttp.py
2025-10-15 01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/pipeline/transport/_aiohttp.py
2025-12-11 19:57:42.000000000 +0100
@@ -474,7 +474,7 @@
except aiohttp.client_exceptions.ClientResponseError as err:
raise ServiceResponseError(err, error=err) from err
except asyncio.TimeoutError as err:
- raise ServiceResponseError(err, error=err) from err
+ raise ServiceResponseTimeoutError(err, error=err) from err
except aiohttp.client_exceptions.ClientError as err:
raise ServiceRequestError(err, error=err) from err
except Exception as err:
@@ -571,7 +571,7 @@
except aiohttp.client_exceptions.ClientResponseError as err:
raise ServiceResponseError(err, error=err) from err
except asyncio.TimeoutError as err:
- raise ServiceResponseError(err, error=err) from err
+ raise ServiceResponseTimeoutError(err, error=err) from err
except aiohttp.client_exceptions.ClientError as err:
raise ServiceRequestError(err, error=err) from err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/azure/core/pipeline/transport/_requests_basic.py
new/azure_core-1.37.0/azure/core/pipeline/transport/_requests_basic.py
--- old/azure_core-1.36.0/azure/core/pipeline/transport/_requests_basic.py
2025-10-15 01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/pipeline/transport/_requests_basic.py
2025-12-11 19:57:42.000000000 +0100
@@ -46,7 +46,9 @@
from azure.core.configuration import ConnectionConfiguration
from azure.core.exceptions import (
ServiceRequestError,
+ ServiceRequestTimeoutError,
ServiceResponseError,
+ ServiceResponseTimeoutError,
IncompleteReadError,
HttpResponseError,
DecodeError,
@@ -85,7 +87,7 @@
except CoreDecodeError as e:
raise DecodeError(e, error=e) from e
except ReadTimeoutError as e:
- raise ServiceRequestError(e, error=e) from e
+ raise ServiceResponseTimeoutError(e, error=e) from e
else:
# Standard file-like object.
while True:
@@ -202,6 +204,14 @@
_LOGGER.warning("Unable to stream download.")
internal_response.close()
raise HttpResponseError(err, error=err) from err
+ except requests.ConnectionError as err:
+ internal_response.close()
+ if err.args and isinstance(err.args[0], ReadTimeoutError):
+ raise ServiceResponseTimeoutError(err, error=err) from err
+ raise ServiceResponseError(err, error=err) from err
+ except requests.RequestException as err:
+ internal_response.close()
+ raise ServiceResponseError(err, error=err) from err
except Exception as err:
_LOGGER.warning("Unable to stream download.")
internal_response.close()
@@ -384,13 +394,14 @@
"Please report this issue to
https://github.com/Azure/azure-sdk-for-python/issues."
) from err
raise
- except (
- NewConnectionError,
- ConnectTimeoutError,
- ) as err:
+ except NewConnectionError as err:
error = ServiceRequestError(err, error=err)
+ except ConnectTimeoutError as err:
+ error = ServiceRequestTimeoutError(err, error=err)
+ except requests.exceptions.ConnectTimeout as err:
+ error = ServiceRequestTimeoutError(err, error=err)
except requests.exceptions.ReadTimeout as err:
- error = ServiceResponseError(err, error=err)
+ error = ServiceResponseTimeoutError(err, error=err)
except requests.exceptions.ConnectionError as err:
if err.args and isinstance(err.args[0], ProtocolError):
error = ServiceResponseError(err, error=err)
@@ -405,7 +416,7 @@
_LOGGER.warning("Unable to stream download.")
error = HttpResponseError(err, error=err)
except requests.RequestException as err:
- error = ServiceRequestError(err, error=err)
+ error = ServiceResponseError(err, error=err)
if error:
raise error
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/azure/core/rest/_aiohttp.py
new/azure_core-1.37.0/azure/core/rest/_aiohttp.py
--- old/azure_core-1.36.0/azure/core/rest/_aiohttp.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/rest/_aiohttp.py 2025-12-11
19:57:42.000000000 +0100
@@ -27,14 +27,23 @@
import asyncio # pylint: disable=do-not-import-asyncio
from itertools import groupby
from typing import Iterator, cast
+
+import aiohttp
from multidict import CIMultiDict
+
from ._http_response_impl_async import (
AsyncHttpResponseImpl,
AsyncHttpResponseBackcompatMixin,
)
from ..pipeline.transport._aiohttp import AioHttpStreamDownloadGenerator
from ..utils._pipeline_transport_rest_shared import _pad_attr_name,
_aiohttp_body_helper
-from ..exceptions import ResponseNotReadError
+from ..exceptions import (
+ ResponseNotReadError,
+ IncompleteReadError,
+ ServiceResponseError,
+ ServiceResponseTimeoutError,
+ ServiceRequestError,
+)
class _ItemsView(collections.abc.ItemsView):
@@ -212,7 +221,18 @@
"""
if not self._content:
self._stream_download_check()
- self._content = await self._internal_response.read()
+ try:
+ self._content = await self._internal_response.read()
+ except aiohttp.client_exceptions.ClientPayloadError as err:
+ # This is the case that server closes connection before we
finish the reading. aiohttp library
+ # raises ClientPayloadError.
+ raise IncompleteReadError(err, error=err) from err
+ except aiohttp.client_exceptions.ClientResponseError as err:
+ raise ServiceResponseError(err, error=err) from err
+ except asyncio.TimeoutError as err:
+ raise ServiceResponseTimeoutError(err, error=err) from err
+ except aiohttp.client_exceptions.ClientError as err:
+ raise ServiceRequestError(err, error=err) from err
await self._set_read_checks()
return _aiohttp_body_helper(self)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/azure/core/serialization.py
new/azure_core-1.37.0/azure/core/serialization.py
--- old/azure_core-1.36.0/azure/core/serialization.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/azure/core/serialization.py 2025-12-11
19:57:42.000000000 +0100
@@ -4,6 +4,7 @@
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------
+# pylint: disable=protected-access
import base64
from functools import partial
from json import JSONEncoder
@@ -19,6 +20,7 @@
"as_attribute_dict",
"attribute_list",
"TypeHandlerRegistry",
+ "get_backcompat_attr_name",
]
TZ_UTC = timezone.utc
@@ -317,7 +319,7 @@
:rtype: bool
"""
try:
- return p._visibility == ["read"] # pylint: disable=protected-access
+ return p._visibility == ["read"]
except AttributeError:
return False
@@ -332,6 +334,20 @@
return as_attribute_dict(v, exclude_readonly=exclude_readonly) if
is_generated_model(v) else v
+def _get_backcompat_name(rest_field: Any, default_attr_name: str) -> str:
+ """Get the backcompat name for an attribute.
+
+ :param any rest_field: The rest field to get the backcompat name from.
+ :param str default_attr_name: The default attribute name to use if no
backcompat name
+ :return: The backcompat name.
+ :rtype: str
+ """
+ try:
+ return rest_field._original_tsp_name or default_attr_name
+ except AttributeError:
+ return default_attr_name
+
+
def _get_flattened_attribute(obj: Any) -> Optional[str]:
"""Get the name of the flattened attribute in a generated TypeSpec model
if one exists.
@@ -348,11 +364,9 @@
if flattened_items is None:
return None
- for k, v in obj._attr_to_rest_field.items(): # pylint:
disable=protected-access
+ for k, v in obj._attr_to_rest_field.items():
try:
- if set(v._class_type._attr_to_rest_field.keys()).intersection( #
pylint: disable=protected-access
- set(flattened_items)
- ):
+ if
set(v._class_type._attr_to_rest_field.keys()).intersection(set(flattened_items)):
return k
except AttributeError:
# if the attribute does not have _class_type, it is not a typespec
generated model
@@ -372,12 +386,12 @@
raise TypeError("Object is not a generated SDK model.")
if hasattr(obj, "_attribute_map"):
# msrest model
- return list(obj._attribute_map.keys()) # pylint:
disable=protected-access
+ return list(obj._attribute_map.keys())
flattened_attribute = _get_flattened_attribute(obj)
retval: List[str] = []
- for attr_name, rest_field in obj._attr_to_rest_field.items(): # pylint:
disable=protected-access
+ for attr_name, rest_field in obj._attr_to_rest_field.items():
if flattened_attribute == attr_name:
- retval.extend(attribute_list(rest_field._class_type)) # pylint:
disable=protected-access
+ retval.extend(attribute_list(rest_field._class_type))
else:
retval.append(attr_name)
return retval
@@ -410,16 +424,16 @@
# create a reverse mapping from rest field name to attribute name
rest_to_attr = {}
flattened_attribute = _get_flattened_attribute(obj)
- for attr_name, rest_field in obj._attr_to_rest_field.items(): #
pylint: disable=protected-access
+ for attr_name, rest_field in obj._attr_to_rest_field.items():
if exclude_readonly and _is_readonly(rest_field):
# if we're excluding readonly properties, we need to track them
- readonly_props.add(rest_field._rest_name) # pylint:
disable=protected-access
+ readonly_props.add(rest_field._rest_name)
if flattened_attribute == attr_name:
- for fk, fv in
rest_field._class_type._attr_to_rest_field.items(): # pylint:
disable=protected-access
- rest_to_attr[fv._rest_name] = fk # pylint:
disable=protected-access
+ for fk, fv in
rest_field._class_type._attr_to_rest_field.items():
+ rest_to_attr[fv._rest_name] = fk
else:
- rest_to_attr[rest_field._rest_name] = attr_name # pylint:
disable=protected-access
+ rest_to_attr[rest_field._rest_name] = attr_name
for k, v in obj.items():
if exclude_readonly and k in readonly_props: # pyright: ignore
continue
@@ -429,10 +443,8 @@
else:
is_multipart_file_input = False
try:
- is_multipart_file_input = next( # pylint:
disable=protected-access
- rf
- for rf in obj._attr_to_rest_field.values() # pylint:
disable=protected-access
- if rf._rest_name == k # pylint:
disable=protected-access
+ is_multipart_file_input = next(
+ rf for rf in obj._attr_to_rest_field.values() if
rf._rest_name == k
)._is_multipart_file_input
except StopIteration:
pass
@@ -444,3 +456,36 @@
except AttributeError as exc:
# not a typespec generated model
raise TypeError("Object must be a generated model instance.") from exc
+
+
+def get_backcompat_attr_name(model: Any, attr_name: str) -> str:
+ """Get the backcompat attribute name for a given attribute.
+
+ This function takes an attribute name and returns the backcompat name
(original TSP name)
+ if one exists, otherwise returns the attribute name itself.
+
+ :param any model: The model instance.
+ :param str attr_name: The attribute name to get the backcompat name for.
+ :return: The backcompat attribute name (original TSP name) or the
attribute name itself.
+ :rtype: str
+ """
+ if not is_generated_model(model):
+ raise TypeError("Object must be a generated model instance.")
+
+ # Check if attr_name exists in the model's attributes
+ flattened_attribute = _get_flattened_attribute(model)
+ for field_attr_name, rest_field in model._attr_to_rest_field.items():
+ # Check if this is the attribute we're looking for
+ if field_attr_name == attr_name:
+ # Return the original TSP name if it exists, otherwise the
attribute name
+ return _get_backcompat_name(rest_field, attr_name)
+
+ # If this is a flattened attribute, check inside it
+ if flattened_attribute == field_attr_name:
+ for fk, fv in rest_field._class_type._attr_to_rest_field.items():
+ if fk == attr_name:
+ # Return the original TSP name for this flattened property
+ return _get_backcompat_name(fv, fk)
+
+ # If not found in the model, just return the attribute name as-is
+ return attr_name
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/azure_core.egg-info/PKG-INFO
new/azure_core-1.37.0/azure_core.egg-info/PKG-INFO
--- old/azure_core-1.36.0/azure_core.egg-info/PKG-INFO 2025-10-15
01:34:50.000000000 +0200
+++ new/azure_core-1.37.0/azure_core.egg-info/PKG-INFO 2025-12-11
19:58:25.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: azure-core
-Version: 1.36.0
+Version: 1.37.0
Summary: Microsoft Azure Core Library for Python
Home-page:
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
Author: Microsoft Corporation
@@ -323,15 +323,25 @@
# Release History
+## 1.37.0 (2025-12-11)
+
+### Features Added
+
+- Added `get_backcompat_attr_name` to `azure.core.serialization`.
`get_backcompat_attr_name` gets the backcompat name of an attribute using
backcompat attribute access. #44084
+
## 1.36.0 (2025-10-14)
### Features Added
- Added `TypeHandlerRegistry` to `azure.core.serialization` to allow
developers to register custom serializers and deserializers for specific types
or conditions. #43051
+### Breaking Changes
+
### Bugs Fixed
- Fixed repeated import attempts of cchardet and chardet when
charset_normalizer is used #43092
+- Fixed leaked requests and aiohttp exceptions for streamed responses #43200
+- Improved granularity of ServiceRequestError and ServiceResponseError
exceptions raised in timeout scenarios from the requests and aiohttp transports
#43200
### Other Changes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/azure_core.egg-info/SOURCES.txt
new/azure_core-1.37.0/azure_core.egg-info/SOURCES.txt
--- old/azure_core-1.36.0/azure_core.egg-info/SOURCES.txt 2025-10-15
01:34:50.000000000 +0200
+++ new/azure_core-1.37.0/azure_core.egg-info/SOURCES.txt 2025-12-11
19:58:25.000000000 +0100
@@ -97,6 +97,8 @@
samples/example_shared_transport_async.py
samples/example_tracing.py
samples/example_tracing_async.py
+samples/example_truststore_injection.py
+samples/example_truststore_ssl_context.py
samples/test_example_async.py
samples/test_example_policies.py
samples/test_example_sansio.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/samples/README.md
new/azure_core-1.37.0/samples/README.md
--- old/azure_core-1.36.0/samples/README.md 2025-10-15 01:34:08.000000000
+0200
+++ new/azure_core-1.37.0/samples/README.md 2025-12-11 19:57:42.000000000
+0100
@@ -22,5 +22,10 @@
These are some code snippets that show the way in which end users can
customize the behavior.
-[shared_transport.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/samples/example_shared_transport.py)
- samples of how to use a shared sync transport
-[shared_transport_async.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/samples/example_shared_transport_async.py)
- samples of how to use a shared async transport
+[example_shared_transport.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/samples/example_shared_transport.py)
- samples of how to use a shared sync transport
+
+[example_shared_transport_async.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/samples/example_shared_transport_async.py)
- samples of how to use a shared async transport
+
+[example_truststore_injection.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/samples/example_truststore_injection.py)
- samples of how to use the `truststore` library to leverage OS certificate
stores via global injection
+
+[example_truststore_ssl_context.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/core/azure-core/samples/example_truststore_ssl_context.py)
- samples of how to use `truststore.SSLContext` directly with custom transport
sessions
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/samples/example_truststore_injection.py
new/azure_core-1.37.0/samples/example_truststore_injection.py
--- old/azure_core-1.36.0/samples/example_truststore_injection.py
1970-01-01 01:00:00.000000000 +0100
+++ new/azure_core-1.37.0/samples/example_truststore_injection.py
2025-12-11 19:57:42.000000000 +0100
@@ -0,0 +1,90 @@
+# 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.
+# --------------------------------------------------------------------------
+
+"""
+Example: Using truststore with Azure SDK for Python (Sync and Async)
+
+This sample demonstrates how to use the `truststore` library to leverage
+operating system certificate stores when working with Azure SDK for Python,
+including both synchronous (requests) and asynchronous (aiohttp)
+HTTP clients.
+
+Requirements:
+ pip install truststore azure-identity azure-storage-blob aiohttp
+USAGE:
+ python example_truststore_injection.py
+"""
+
+import asyncio
+import truststore
+
+# Inject truststore BEFORE importing Azure SDK libraries
+truststore.inject_into_ssl()
+
+# Synchronous imports
+from azure.identity import DefaultAzureCredential
+from azure.storage.blob import BlobServiceClient
+
+# Asynchronous imports
+from azure.identity.aio import DefaultAzureCredential as
AsyncDefaultAzureCredential
+from azure.storage.blob.aio import BlobServiceClient as AsyncBlobServiceClient
+
+
+# =============================================================================
+# Synchronous Example
+# =============================================================================
+
+
+def sync_blob_storage_example():
+ """Synchronous Azure Blob Storage with system certificates."""
+
+ account_url = "https://<your-storage-account>.blob.core.windows.net"
+
+ credential = DefaultAzureCredential()
+ blob_service_client = BlobServiceClient(account_url=account_url,
credential=credential)
+
+ print("=== Sync Blob Storage ===")
+ for container in blob_service_client.list_containers():
+ print(f"Container: {container['name']}")
+
+ blob_service_client.close()
+ credential.close()
+
+
+# =============================================================================
+# Asynchronous Example (uses aiohttp underneath)
+# =============================================================================
+
+
+async def async_blob_storage_example():
+ """Asynchronous Azure Blob Storage with system certificates (aiohttp)."""
+
+ account_url = "https://<your-storage-account>.blob.core.windows.net"
+
+ # Async credential and client - aiohttp will use the injected truststore
+ credential = AsyncDefaultAzureCredential()
+ blob_service_client = AsyncBlobServiceClient(account_url=account_url,
credential=credential)
+
+ print("=== Async Blob Storage ===")
+ async for container in blob_service_client.list_containers():
+ print(f"Container: {container['name']}")
+
+ # Always close async clients
+ await blob_service_client.close()
+ await credential.close()
+
+
+async def main():
+ # Run async example
+ await async_blob_storage_example()
+
+
+if __name__ == "__main__":
+ sync_blob_storage_example()
+ asyncio.run(main())
+ truststore.extract_from_ssl()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/samples/example_truststore_ssl_context.py
new/azure_core-1.37.0/samples/example_truststore_ssl_context.py
--- old/azure_core-1.36.0/samples/example_truststore_ssl_context.py
1970-01-01 01:00:00.000000000 +0100
+++ new/azure_core-1.37.0/samples/example_truststore_ssl_context.py
2025-12-11 19:57:42.000000000 +0100
@@ -0,0 +1,143 @@
+# 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.
+# --------------------------------------------------------------------------
+
+"""
+Example: Using truststore.SSLContext with Azure SDK for Python (Sync and Async)
+
+This sample demonstrates how to use `truststore.SSLContext` directly to
leverage
+operating system certificate stores when working with Azure SDK for Python.
+
+Unlike `truststore.inject_into_ssl()` which globally patches the ssl module,
+this approach gives you more control over which connections use system
certificates
+by creating custom transport sessions with a truststore-based SSLContext.
+
+Requirements:
+ pip install truststore azure-identity azure-storage-blob aiohttp requests
+USAGE:
+ python example_truststore_ssl_context.py
+"""
+
+import ssl
+import asyncio
+import truststore
+import requests
+import aiohttp
+from requests.adapters import HTTPAdapter
+
+from azure.identity import DefaultAzureCredential
+from azure.identity.aio import DefaultAzureCredential as
AsyncDefaultAzureCredential
+from azure.storage.blob import BlobServiceClient
+from azure.storage.blob.aio import BlobServiceClient as AsyncBlobServiceClient
+from azure.core.pipeline.transport import RequestsTransport, AioHttpTransport
+
+
+# =============================================================================
+# Helper: Custom HTTPAdapter for requests
+# =============================================================================
+
+
+class TruststoreHTTPAdapter(HTTPAdapter):
+ """Custom HTTPAdapter that uses truststore.SSLContext for SSL
verification."""
+
+ def __init__(self, ssl_context, **kwargs):
+ self._ssl_context = ssl_context
+ super().__init__(**kwargs)
+
+ def init_poolmanager(self, *args, **kwargs):
+ kwargs["ssl_context"] = self._ssl_context
+ return super().init_poolmanager(*args, **kwargs)
+
+
+# =============================================================================
+# Synchronous Example
+# =============================================================================
+
+
+def sync_blob_storage_with_ssl_context():
+ """Synchronous Azure Blob Storage using truststore.SSLContext directly.
+
+ This approach gives you more control over the SSL context configuration
+ without globally injecting truststore into ssl module.
+ """
+
+ account_url = "https://<your-storage-account>.blob.core.windows.net"
+
+ # Create SSLContext using truststore (uses system certificate stores)
+ ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+
+ # Create a session with truststore adapter
+ session = requests.Session()
+ adapter = TruststoreHTTPAdapter(ssl_context)
+ session.mount("https://", adapter)
+
+ # Create transport with the custom session
+ transport = RequestsTransport(session=session, session_owner=False)
+
+ with transport:
+ credential = DefaultAzureCredential()
+ blob_service_client = BlobServiceClient(
+ account_url=account_url,
+ credential=credential,
+ transport=transport,
+ )
+
+ print("=== Sync Blob Storage with SSLContext ===")
+ for container in blob_service_client.list_containers():
+ print(f"Container: {container['name']}")
+
+ blob_service_client.close()
+ credential.close()
+
+ session.close() # Close the session manually since session_owner=False
+
+
+# =============================================================================
+# Asynchronous Example
+# =============================================================================
+
+
+async def async_blob_storage_with_ssl_context():
+ """Asynchronous Azure Blob Storage using truststore.SSLContext directly.
+
+ This approach gives you more control over the SSL context configuration
+ without globally injecting truststore into ssl module.
+ """
+
+ account_url = "https://<your-storage-account>.blob.core.windows.net"
+
+ # Create SSLContext using truststore (uses system certificate stores)
+ ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+
+ # Create a TCPConnector with our truststore SSLContext
+ connector = aiohttp.TCPConnector(ssl=ssl_context)
+ session = aiohttp.ClientSession(connector=connector)
+
+ # Create transport with the custom session
+ transport = AioHttpTransport(session=session, session_owner=False)
+
+ async with transport:
+ credential = AsyncDefaultAzureCredential()
+ blob_service_client = AsyncBlobServiceClient(
+ account_url=account_url,
+ credential=credential,
+ transport=transport,
+ )
+
+ print("=== Async Blob Storage with SSLContext ===")
+ async for container in blob_service_client.list_containers():
+ print(f"Container: {container['name']}")
+
+ await blob_service_client.close()
+ await credential.close()
+
+ await session.close() # Close the session manually since
session_owner=False
+
+
+if __name__ == "__main__":
+ sync_blob_storage_with_ssl_context()
+ asyncio.run(async_blob_storage_with_ssl_context())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/tests/async_tests/test_basic_transport_async.py
new/azure_core-1.37.0/tests/async_tests/test_basic_transport_async.py
--- old/azure_core-1.36.0/tests/async_tests/test_basic_transport_async.py
2025-10-15 01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/tests/async_tests/test_basic_transport_async.py
2025-12-11 19:57:42.000000000 +0100
@@ -3,6 +3,15 @@
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# -------------------------------------------------------------------------
+
+import pytest
+import sys
+import asyncio
+from packaging.version import Version
+from unittest import mock
+
+import aiohttp
+
from azure.core.pipeline.transport import (
AsyncHttpResponse as PipelineTransportAsyncHttpResponse,
AsyncHttpTransport,
@@ -21,13 +30,8 @@
ServiceRequestTimeoutError,
ServiceResponseTimeoutError,
)
+
from utils import HTTP_REQUESTS, request_and_responses_product
-import pytest
-import sys
-import asyncio
-from unittest.mock import Mock
-from packaging.version import Version
-import aiohttp
# transport = mock.MagicMock(spec=AsyncHttpTransport)
@@ -1049,47 +1053,66 @@
assert result # No exception is good enough here
[email protected](
- Version(aiohttp.__version__) >= Version("3.10"),
- reason="aiohttp 3.10 introduced separate connection timeout",
-)
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
@pytest.mark.asyncio
-async def test_aiohttp_timeout_response(http_request):
+async def test_aiohttp_timeout_response(port, http_request):
async with AioHttpTransport() as transport:
- transport.session._connector.connect =
Mock(side_effect=asyncio.TimeoutError("Too slow!"))
-
- request = http_request("GET", f"http://localhost:12345/basic/string")
-
- with pytest.raises(ServiceResponseTimeoutError) as err:
- await transport.send(request)
- with pytest.raises(ServiceResponseError) as err:
- await transport.send(request)
+ request = http_request("GET", f"http://localhost:{port}/basic/string")
- stream_request = http_request("GET",
f"http://localhost:12345/streams/basic")
- with pytest.raises(ServiceResponseTimeoutError) as err:
- await transport.send(stream_request, stream=True)
+ with mock.patch.object(
+ aiohttp.ClientResponse, "start",
side_effect=asyncio.TimeoutError("Too slow!")
+ ) as mock_method:
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ await transport.send(request)
+
+ with pytest.raises(ServiceResponseError) as err:
+ await transport.send(request)
+
+ stream_resp = http_request("GET",
f"http://localhost:{port}/streams/basic")
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ await transport.send(stream_resp, stream=True)
+
+ stream_resp = await transport.send(stream_resp, stream=True)
+ with mock.patch.object(
+ aiohttp.streams.StreamReader, "read",
side_effect=asyncio.TimeoutError("Too slow!")
+ ) as mock_method:
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ try:
+ # current HttpResponse
+ await stream_resp.read()
+ except AttributeError:
+ # legacy HttpResponse
+ b"".join([b async for b in
stream_resp.stream_download(None)])
[email protected](
- Version(aiohttp.__version__) < Version("3.10"),
- reason="aiohttp 3.10 introduced separate connection timeout",
-)
@pytest.mark.parametrize("http_request", HTTP_REQUESTS)
@pytest.mark.asyncio
async def test_aiohttp_timeout_request(http_request):
async with AioHttpTransport() as transport:
- transport.session._connector.connect =
Mock(side_effect=asyncio.TimeoutError("Too slow!"))
+ transport.session._connector.connect =
mock.Mock(side_effect=asyncio.TimeoutError("Too slow!"))
request = http_request("GET", f"http://localhost:12345/basic/string")
- with pytest.raises(ServiceRequestTimeoutError) as err:
- await transport.send(request)
-
- with pytest.raises(ServiceRequestError) as err:
- await transport.send(request)
-
- stream_request = http_request("GET",
f"http://localhost:12345/streams/basic")
- with pytest.raises(ServiceRequestTimeoutError) as err:
- await transport.send(stream_request, stream=True)
+ # aiohttp 3.10 introduced separate connection timeout
+ if Version(aiohttp.__version__) >= Version("3.10"):
+ with pytest.raises(ServiceRequestTimeoutError) as err:
+ await transport.send(request)
+
+ with pytest.raises(ServiceRequestError) as err:
+ await transport.send(request)
+
+ stream_request = http_request("GET",
f"http://localhost:12345/streams/basic")
+ with pytest.raises(ServiceRequestTimeoutError) as err:
+ await transport.send(stream_request, stream=True)
+
+ else:
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ await transport.send(request)
+
+ with pytest.raises(ServiceResponseError) as err:
+ await transport.send(request)
+
+ stream_request = http_request("GET",
f"http://localhost:12345/streams/basic")
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ await transport.send(stream_request, stream=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/azure_core-1.36.0/tests/specs_sdk/modeltypes/modeltypes/_utils/model_base.py
new/azure_core-1.37.0/tests/specs_sdk/modeltypes/modeltypes/_utils/model_base.py
---
old/azure_core-1.36.0/tests/specs_sdk/modeltypes/modeltypes/_utils/model_base.py
2025-10-15 01:34:08.000000000 +0200
+++
new/azure_core-1.37.0/tests/specs_sdk/modeltypes/modeltypes/_utils/model_base.py
2025-12-11 19:57:42.000000000 +0100
@@ -654,6 +654,10 @@
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._backcompat_attr_to_rest_field: typing.Dict[str, _RestField] =
{
+ Model._get_backcompat_attribute_name(cls._attr_to_rest_field,
attr): rf
+ for attr, rf in cls._attr_to_rest_field.items()
+ }
cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
return super().__new__(cls)
@@ -664,6 +668,16 @@
base.__mapping__[discriminator or cls.__name__] = cls # type:
ignore
@classmethod
+ def _get_backcompat_attribute_name(cls, _attr_to_rest_field:
typing.Dict[str, "_RestField"], attr_name: str) -> str:
+ rest_field = _attr_to_rest_field.get(attr_name) # pylint:
disable=protected-access
+ if rest_field is None:
+ return attr_name
+ original_tsp_name = getattr(rest_field, "_original_tsp_name", None) #
pylint: disable=protected-access
+ if original_tsp_name:
+ return original_tsp_name
+ return attr_name
+
+ @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:
@@ -998,6 +1012,7 @@
format: typing.Optional[str] = None,
is_multipart_file_input: bool = False,
xml: typing.Optional[typing.Dict[str, typing.Any]] = None,
+ original_tsp_name: typing.Optional[str] = None,
):
self._type = type
self._rest_name_input = name
@@ -1009,6 +1024,7 @@
self._format = format
self._is_multipart_file_input = is_multipart_file_input
self._xml = xml if xml is not None else {}
+ self._original_tsp_name = original_tsp_name
@property
def _class_type(self) -> typing.Any:
@@ -1060,6 +1076,7 @@
format: typing.Optional[str] = None,
is_multipart_file_input: bool = False,
xml: typing.Optional[typing.Dict[str, typing.Any]] = None,
+ original_tsp_name: typing.Optional[str] = None,
) -> typing.Any:
return _RestField(
name=name,
@@ -1069,6 +1086,7 @@
format=format,
is_multipart_file_input=is_multipart_file_input,
xml=xml,
+ original_tsp_name=original_tsp_name,
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/tests/test_basic_transport.py
new/azure_core-1.37.0/tests/test_basic_transport.py
--- old/azure_core-1.36.0/tests/test_basic_transport.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/tests/test_basic_transport.py 2025-12-11
19:57:42.000000000 +0100
@@ -5,28 +5,35 @@
# -------------------------------------------------------------------------
from http.client import HTTPConnection
from collections import OrderedDict
-import sys
+import logging
+import pytest
+from unittest import mock
+from socket import timeout as SocketTimeout
-try:
- from unittest import mock
-except ImportError:
- import mock
+from urllib3.util import connection as urllib_connection
+from urllib3.response import HTTPResponse as UrllibResponse
+from urllib3.connection import HTTPConnection as UrllibConnection
+from azure.core.rest._http_response_impl import HttpResponseImpl as
RestHttpResponseImpl
+from azure.core.pipeline._tools import is_rest
from azure.core.pipeline.transport import HttpResponse as
PipelineTransportHttpResponse, RequestsTransport
from azure.core.pipeline.transport._base import HttpTransport,
_deserialize_response, _urljoin
from azure.core.pipeline.policies import HeadersPolicy
from azure.core.pipeline import Pipeline
-from azure.core.exceptions import HttpResponseError
-import logging
-import pytest
+from azure.core.exceptions import (
+ HttpResponseError,
+ ServiceRequestError,
+ ServiceResponseError,
+ ServiceRequestTimeoutError,
+ ServiceResponseTimeoutError,
+)
+
from utils import (
HTTP_REQUESTS,
request_and_responses_product,
HTTP_CLIENT_TRANSPORT_RESPONSES,
create_transport_response,
)
-from azure.core.rest._http_response_impl import HttpResponseImpl as
RestHttpResponseImpl
-from azure.core.pipeline._tools import is_rest
class PipelineTransportMockResponse(PipelineTransportHttpResponse):
@@ -1322,3 +1329,49 @@
result = transport.send(request)
assert result # No exception is good enough here
+
+
[email protected]("http_request", HTTP_REQUESTS)
+def test_requests_timeout_response(caplog, port, http_request):
+ transport = RequestsTransport()
+
+ request = http_request("GET", f"http://localhost:{port}/basic/string")
+
+ with mock.patch.object(UrllibConnection, "getresponse",
side_effect=SocketTimeout) as mock_method:
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ transport.send(request, read_timeout=0.0001)
+
+ with pytest.raises(ServiceResponseError) as err:
+ transport.send(request, read_timeout=0.0001)
+
+ stream_request = http_request("GET",
f"http://localhost:{port}/streams/basic")
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ transport.send(stream_request, stream=True, read_timeout=0.0001)
+
+ stream_resp = transport.send(stream_request, stream=True)
+ with mock.patch.object(UrllibResponse, "_handle_chunk",
side_effect=SocketTimeout) as mock_method:
+ with pytest.raises(ServiceResponseTimeoutError) as err:
+ try:
+ # current HttpResponse
+ stream_resp.read()
+ except AttributeError:
+ # legacy HttpResponse
+ b"".join(stream_resp.stream_download(None))
+
+
[email protected]("http_request", HTTP_REQUESTS)
+def test_requests_timeout_request(caplog, port, http_request):
+ transport = RequestsTransport()
+
+ request = http_request("GET", f"http://localhost:{port}/basic/string")
+
+ with mock.patch.object(urllib_connection, "create_connection",
side_effect=SocketTimeout) as mock_method:
+ with pytest.raises(ServiceRequestTimeoutError) as err:
+ transport.send(request, connection_timeout=0.0001)
+
+ with pytest.raises(ServiceRequestTimeoutError) as err:
+ transport.send(request, connection_timeout=0.0001)
+
+ stream_request = http_request("GET",
f"http://localhost:{port}/streams/basic")
+ with pytest.raises(ServiceRequestTimeoutError) as err:
+ transport.send(stream_request, stream=True,
connection_timeout=0.0001)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/tests/test_serialization.py
new/azure_core-1.37.0/tests/test_serialization.py
--- old/azure_core-1.36.0/tests/test_serialization.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/tests/test_serialization.py 2025-12-11
19:57:42.000000000 +0100
@@ -7,11 +7,17 @@
from enum import Enum
import json
import sys
-import traceback
from typing import Any, Dict, List, Optional, Union, Type
from io import BytesIO
-from azure.core.serialization import AzureJSONEncoder, NULL,
as_attribute_dict, is_generated_model, attribute_list
+from azure.core.serialization import (
+ AzureJSONEncoder,
+ NULL,
+ as_attribute_dict,
+ get_backcompat_attr_name,
+ is_generated_model,
+ attribute_list,
+)
from azure.core.exceptions import DeserializationError
import pytest
from modeltypes._utils.model_base import (
@@ -1644,3 +1650,463 @@
deserialized_a = _deserialize(ModelA, json_dict_a)
assert isinstance(deserialized_a, ModelA)
assert deserialized_a.type == "A2"
+
+
+class TestBackcompatPropertyMatrix:
+ """
+ Systematic test matrix for DPG model property backcompat scenarios.
+
+ Tests all combinations of 5 key dimensions:
+ 1. wireName: same/different from attr_name
+ 2. attr_name: normal/padded (reserved word)
+ 3. original_tsp_name: None/present (TSP name before padding)
+ 4. visibility: readonly/readwrite (affects exclude_readonly)
+ 5. structure: regular/nested/flattened models
+
+ COMPLETE TEST MATRIX:
+
┌───────┬─────────────┬──────────────┬─────────────────┬────────────┬──────────────┬─────────────────────────────┐
+ │ Test │ Wire Name │ Attr Name │ Original TSP │ Visibility │
Structure │ Expected Behavior │
+
├───────┼─────────────┼──────────────┼─────────────────┼────────────┼──────────────┼─────────────────────────────┤
+ │ 1a │ same │ normal │ None │ readwrite │
regular │ attr_name │
+ │ 1b │ same │ normal │ None │ readonly │
regular │ attr_name (exclude test) │
+ │ 2a │ different │ normal │ None │ readwrite │
regular │ attr_name │
+ │ 2b │ different │ normal │ None │ readonly │
regular │ attr_name (exclude test) │
+ │ 3a │ same │ padded │ present │ readwrite │
regular │ original_tsp_name │
+ │ 3b │ same │ padded │ present │ readonly │
regular │ original_tsp_name (exclude) │
+ │ 4a │ different │ padded │ present │ readwrite │
regular │ original_tsp_name │
+ │ 4b │ different │ padded │ present │ readonly │
regular │ original_tsp_name (exclude) │
+ │ 5a │ various │ mixed │ mixed │ mixed │
nested │ recursive backcompat │
+ │ 6a │ same │ padded │ present │ readwrite │
flat-contain │ flattened + backcompat │
+ │ 6b │ various │ mixed │ mixed │ mixed │
flat-props │ flattened props backcompat │
+ │ 6c │ various │ mixed │ mixed │ readonly │
flat-mixed │ flattened + exclude │
+
└───────┴─────────────┴──────────────┴─────────────────┴────────────┴──────────────┴─────────────────────────────┘
+ """
+
+ # ========== DIMENSION 1-4 COMBINATIONS: REGULAR STRUCTURE ==========
+
+ def test_1a_same_wire_normal_attr_no_original_readwrite_regular(self):
+ """Wire=attr, normal attr, no original, readwrite, regular model"""
+
+ class RegularModel(HybridModel):
+ field_name: str = rest_field()
+
+ model = RegularModel(field_name="value")
+
+ # Should use attr_name (same as wire name)
+ assert attribute_list(model) == ["field_name"]
+ assert as_attribute_dict(model) == {"field_name": "value"}
+ assert as_attribute_dict(model, exclude_readonly=True) ==
{"field_name": "value"}
+ assert getattr(model, "field_name") == "value"
+ assert get_backcompat_attr_name(model, "field_name") == "field_name"
+
+ def test_1b_same_wire_normal_attr_no_original_readonly_regular(self):
+ """Wire=attr, normal attr, no original, readonly, regular model"""
+
+ class ReadonlyModel(HybridModel):
+ field_name: str = rest_field(visibility=["read"])
+
+ model = ReadonlyModel(field_name="value")
+
+ # Should use attr_name, but excluded when exclude_readonly=True
+ assert attribute_list(model) == ["field_name"]
+ assert as_attribute_dict(model) == {"field_name": "value"}
+ assert as_attribute_dict(model, exclude_readonly=True) == {}
+ assert getattr(model, "field_name") == "value"
+ assert get_backcompat_attr_name(model, "field_name") == "field_name"
+
+ def test_2a_different_wire_normal_attr_no_original_readwrite_regular(self):
+ """Wire≠attr, normal attr, no original, readwrite, regular model"""
+
+ class DifferentWireModel(HybridModel):
+ client_field: str = rest_field(name="wireField")
+
+ model = DifferentWireModel(client_field="value")
+
+ # Should use attr_name (wire name is different)
+ assert attribute_list(model) == ["client_field"]
+ assert as_attribute_dict(model) == {"client_field": "value"}
+ # Verify wire representation uses different name
+ assert dict(model) == {"wireField": "value"}
+ assert getattr(model, "client_field") == "value"
+ assert get_backcompat_attr_name(model, "client_field") ==
"client_field"
+
+ def test_2b_different_wire_normal_attr_no_original_readonly_regular(self):
+ """Wire≠attr, normal attr, no original, readonly, regular model"""
+
+ class ReadonlyDifferentWireModel(HybridModel):
+ client_field: str = rest_field(name="wireField",
visibility=["read"])
+
+ model = ReadonlyDifferentWireModel(client_field="value")
+
+ # Should use attr_name, excluded when exclude_readonly=True
+ assert attribute_list(model) == ["client_field"]
+ assert as_attribute_dict(model) == {"client_field": "value"}
+ assert as_attribute_dict(model, exclude_readonly=True) == {}
+ assert getattr(model, "client_field") == "value"
+ assert get_backcompat_attr_name(model, "client_field") ==
"client_field"
+
+ def test_3a_same_wire_padded_attr_with_original_readwrite_regular(self):
+ """Wire=original, padded attr, original present, readwrite, regular
model"""
+
+ class PaddedModel(HybridModel):
+ keys_property: str = rest_field(original_tsp_name="keys")
+
+ model = PaddedModel(keys_property="value")
+
+ # Should use original_tsp_name when available
+ assert attribute_list(model) == ["keys_property"]
+ assert as_attribute_dict(model) == {"keys_property": "value"}
+ assert get_backcompat_attr_name(model, "keys_property") == "keys"
+ assert getattr(model, "keys_property") == "value"
+ assert set(model.keys()) == {"keys_property"}
+
+ def test_3b_same_wire_padded_attr_with_original_readonly_regular(self):
+ """Wire=original, padded attr, original present, readonly, regular
model"""
+
+ class ReadonlyPaddedModel(HybridModel):
+ keys_property: str = rest_field(visibility=["read"],
original_tsp_name="keys")
+
+ model = ReadonlyPaddedModel(keys_property="value")
+
+ assert attribute_list(model) == ["keys_property"]
+ assert as_attribute_dict(model) == {"keys_property": "value"}
+ assert as_attribute_dict(model, exclude_readonly=True) == {}
+ assert get_backcompat_attr_name(model, "keys_property") == "keys"
+ assert getattr(model, "keys_property") == "value"
+ assert set(model.keys()) == {"keys_property"}
+
+ def
test_4a_different_wire_padded_attr_with_original_readwrite_regular(self):
+ """Wire≠original, padded attr, original present, readwrite, regular
model"""
+
+ class DifferentWirePaddedModel(HybridModel):
+ clear_property: str = rest_field(name="clearWire",
original_tsp_name="clear")
+
+ model = DifferentWirePaddedModel(clear_property="value")
+
+ assert attribute_list(model) == ["clear_property"]
+ assert as_attribute_dict(model) == {"clear_property": "value"}
+ # Verify wire uses different name
+ assert dict(model) == {"clearWire": "value"}
+ assert getattr(model, "clear_property") == "value"
+ assert set(model.keys()) == {"clearWire"}
+
+ def
test_4b_different_wire_padded_attr_with_original_readonly_regular(self):
+ """Wire≠original, padded attr, original present, readonly, regular
model"""
+
+ class ReadonlyDifferentWirePaddedModel(HybridModel):
+ pop_property: str = rest_field(name="popWire",
visibility=["read"], original_tsp_name="pop")
+
+ model = ReadonlyDifferentWirePaddedModel(pop_property="value")
+
+ assert attribute_list(model) == ["pop_property"]
+ assert as_attribute_dict(model) == {"pop_property": "value"}
+ assert as_attribute_dict(model, exclude_readonly=True) == {}
+ assert getattr(model, "pop_property") == "value"
+ assert set(model.keys()) == {"popWire"}
+
+ # ========== DIMENSION 5: STRUCTURE VARIATIONS ==========
+
+ def test_5a_nested_model_backcompat_recursive(self):
+ """Nested models with mixed backcompat scenarios"""
+
+ class NestedBackcompatModel(HybridModel):
+ keys_property: str = rest_field(name="keysWire",
original_tsp_name="keys")
+ normal_field: str = rest_field(name="normalWire")
+
+ class ParentModel(HybridModel):
+ nested: NestedBackcompatModel = rest_field()
+ items_property: str = rest_field(name="itemsWire",
original_tsp_name="items")
+
+ nested_model = NestedBackcompatModel(keys_property="nested_keys",
normal_field="nested_normal")
+ parent_model = ParentModel(nested=nested_model,
items_property="parent_items")
+
+ # Test nested model independently
+ nested_attrs = attribute_list(nested_model)
+ assert set(nested_attrs) == {"keys_property", "normal_field"}
+
+ nested_dict = as_attribute_dict(nested_model)
+ assert nested_dict == {"keys_property": "nested_keys", "normal_field":
"nested_normal"}
+
+ # Test parent model with recursive backcompat
+ parent_attrs = attribute_list(parent_model)
+ assert set(parent_attrs) == {"nested", "items_property"}
+
+ parent_dict = as_attribute_dict(parent_model)
+ expected_parent = {
+ "nested": {"keys_property": "nested_keys", "normal_field":
"nested_normal"},
+ "items_property": "parent_items",
+ }
+ assert parent_dict == expected_parent
+
+ assert getattr(nested_model, "keys_property") == "nested_keys"
+ assert getattr(parent_model, "items_property") == "parent_items"
+
+ assert set(nested_model.keys()) == {"keysWire", "normalWire"}
+ assert set(nested_model.items()) == {("keysWire", "nested_keys"),
("normalWire", "nested_normal")}
+ assert set(parent_model.keys()) == {"nested", "itemsWire"}
+ assert len(parent_model.items()) == 2
+ assert ("nested", parent_model.nested) in parent_model.items()
+ assert ("itemsWire", "parent_items") in parent_model.items()
+
+ def test_6a_flattened_container_with_backcompat(self):
+ """Flattened property where container has backcompat (keys_property →
keys)"""
+
+ # Helper model for flattening content
+ class ContentModel(HybridModel):
+ name: str = rest_field()
+ description: str = rest_field()
+
+ class FlattenedContainerModel(HybridModel):
+ id: str = rest_field()
+ update_property: ContentModel =
rest_field(original_tsp_name="update")
+
+ __flattened_items = ["name", "description"]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ _flattened_input = {k: kwargs.pop(k) for k in kwargs.keys() &
self.__flattened_items}
+ super().__init__(*args, **kwargs)
+ for k, v in _flattened_input.items():
+ setattr(self, k, v)
+
+ def __getattr__(self, name: str) -> Any:
+ if name in self.__flattened_items:
+ if self.update_property is None:
+ return None
+ return getattr(self.update_property, name)
+ raise AttributeError(f"'{self.__class__.__name__}' object has
no attribute '{name}'")
+
+ def __setattr__(self, key: str, value: Any) -> None:
+ if key in self.__flattened_items:
+ if self.update_property is None:
+ self.update_property =
self._attr_to_rest_field["update_property"]._class_type()
+ setattr(self.update_property, key, value)
+ else:
+ super().__setattr__(key, value)
+
+ model = FlattenedContainerModel(id="test_id", name="flattened_name",
description="flattened_desc")
+
+ # Flattened items should appear at top level
+ attrs = attribute_list(model)
+ assert set(attrs) == {"id", "name", "description"}
+ assert getattr(model, "name") == "flattened_name"
+ assert getattr(model, "description") == "flattened_desc"
+
+ # Flattened dict should use top-level names
+ attr_dict = as_attribute_dict(model)
+ expected = {"id": "test_id", "name": "flattened_name", "description":
"flattened_desc"}
+ assert attr_dict == expected
+
+ assert get_backcompat_attr_name(model, "update_property") == "update"
+
+ assert set(model.keys()) == {"id", "update_property"}
+
+ def test_6b_flattened_properties_with_backcompat(self):
+ """Flattened properties themselves have backcompat (type_property →
type)"""
+
+ class BackcompatContentModel(HybridModel):
+ values_property: str = rest_field(name="valuesWire",
original_tsp_name="values")
+ get_property: str = rest_field(name="getWire",
original_tsp_name="get")
+
+ class FlattenedPropsBackcompatModel(HybridModel):
+ name: str = rest_field()
+ properties: BackcompatContentModel = rest_field()
+
+ __flattened_items = ["values_property", "get_property"]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ _flattened_input = {k: kwargs.pop(k) for k in kwargs.keys() &
self.__flattened_items}
+ super().__init__(*args, **kwargs)
+ for k, v in _flattened_input.items():
+ setattr(self, k, v)
+
+ def __getattr__(self, name: str) -> Any:
+ if name in self.__flattened_items:
+ if self.properties is None:
+ return None
+ return getattr(self.properties, name)
+ raise AttributeError(f"'{self.__class__.__name__}' object has
no attribute '{name}'")
+
+ def __setattr__(self, key: str, value: Any) -> None:
+ if key in self.__flattened_items:
+ if self.properties is None:
+ self.properties =
self._attr_to_rest_field["properties"]._class_type()
+ setattr(self.properties, key, value)
+ else:
+ super().__setattr__(key, value)
+
+ model = FlattenedPropsBackcompatModel(
+ name="test_name", values_property="test_values",
get_property="test_class"
+ )
+
+ # Should use original names for flattened properties
+ attrs = attribute_list(model)
+ assert set(attrs) == {"name", "values_property", "get_property"}
+ assert get_backcompat_attr_name(model, "values_property") == "values"
+ assert "test_name" in model.values()
+
+ attr_dict = as_attribute_dict(model)
+ expected = {"name": "test_name", "values_property": "test_values",
"get_property": "test_class"}
+ assert attr_dict == expected
+
+ def test_6c_flattened_with_readonly_exclusion(self):
+ """Flattened model with readonly properties and exclude_readonly
behavior"""
+
+ class ReadonlyContentModel(HybridModel):
+ setdefault_property: str = rest_field(name="readonlyWire",
original_tsp_name="setdefault")
+ popitem_property: str = rest_field(name="readwriteWire",
original_tsp_name="popitem")
+
+ class FlattenedReadonlyModel(HybridModel):
+ get_property: str = rest_field(name="getProperty",
original_tsp_name="get", visibility=["read"])
+ properties: ReadonlyContentModel = rest_field()
+
+ __flattened_items = ["setdefault_property", "popitem_property"]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ _flattened_input = {k: kwargs.pop(k) for k in kwargs.keys() &
self.__flattened_items}
+ super().__init__(*args, **kwargs)
+ for k, v in _flattened_input.items():
+ setattr(self, k, v)
+
+ def __getattr__(self, name: str) -> Any:
+ if name in self.__flattened_items:
+ if self.properties is None:
+ return None
+ return getattr(self.properties, name)
+ raise AttributeError(f"'{self.__class__.__name__}' object has
no attribute '{name}'")
+
+ def __setattr__(self, key: str, value: Any) -> None:
+ if key in self.__flattened_items:
+ if self.properties is None:
+ self.properties =
self._attr_to_rest_field["properties"]._class_type()
+ setattr(self.properties, key, value)
+ else:
+ super().__setattr__(key, value)
+
+ model = FlattenedReadonlyModel(
+ get_property="test_get", setdefault_property="setdefault",
popitem_property="readwrite_value"
+ )
+
+ # All properties included by default
+ full_dict = as_attribute_dict(model, exclude_readonly=False)
+ expected_full = {
+ "get_property": "test_get",
+ "setdefault_property": "setdefault",
+ "popitem_property": "readwrite_value",
+ }
+ assert full_dict == expected_full
+
+ # Readonly properties excluded when requested
+ filtered_dict = as_attribute_dict(model, exclude_readonly=True)
+ expected_filtered = {"setdefault_property": "setdefault",
"popitem_property": "readwrite_value"}
+ assert filtered_dict == expected_filtered
+
+ attribute_list_result = attribute_list(model)
+ expected_attrs = {"get_property", "setdefault_property",
"popitem_property"}
+ assert set(attribute_list_result) == expected_attrs
+ assert get_backcompat_attr_name(model, "setdefault_property") ==
"setdefault"
+ assert get_backcompat_attr_name(model, "popitem_property") == "popitem"
+ assert getattr(model, "get_property") == "test_get"
+
+ # ========== EDGE CASES ==========
+
+ def test_mixed_combinations_comprehensive(self):
+ """Comprehensive test mixing all backcompat scenarios in one model"""
+
+ class ComprehensiveModel(HybridModel):
+ # Case 1: Normal field, same wire name, no original
+ normal_field: str = rest_field()
+
+ # Case 2: Normal field, different wire name, no original
+ different_wire: str = rest_field(name="wireNameDifferent")
+
+ # Case 3: Padded field with original, same wire name
+ keys_property: str = rest_field(original_tsp_name="keys")
+
+ # Case 4: Padded field with original, different wire name
+ values_property: str = rest_field(name="valuesWire",
original_tsp_name="values")
+
+ # Case 5: Readonly field with original
+ items_property: str = rest_field(name="itemsWire",
visibility=["read"], original_tsp_name="items")
+
+ model = ComprehensiveModel(
+ normal_field="normal",
+ different_wire="different",
+ keys_property="keys_val",
+ values_property="values_val",
+ items_property="items_val",
+ )
+
+ # attribute_list should use backcompat names where available
+ attrs = attribute_list(model)
+ expected_attrs = {"normal_field", "different_wire", "keys_property",
"values_property", "items_property"}
+ assert set(attrs) == expected_attrs
+ assert get_backcompat_attr_name(model, "keys_property") == "keys"
+ assert get_backcompat_attr_name(model, "values_property") == "values"
+ assert get_backcompat_attr_name(model, "items_property") == "items"
+ assert get_backcompat_attr_name(model, "normal_field") ==
"normal_field"
+ assert get_backcompat_attr_name(model, "different_wire") ==
"different_wire"
+ assert getattr(model, "keys_property") == "keys_val"
+ assert getattr(model, "values_property") == "values_val"
+ assert getattr(model, "items_property") == "items_val"
+ assert getattr(model, "normal_field") == "normal"
+ assert getattr(model, "different_wire") == "different"
+
+ # Full as_attribute_dict
+ full_dict = as_attribute_dict(model)
+ expected_full = {
+ "normal_field": "normal",
+ "different_wire": "different",
+ "keys_property": "keys_val",
+ "values_property": "values_val",
+ "items_property": "items_val",
+ }
+ assert full_dict == expected_full
+
+ # Exclude readonly
+ filtered_dict = as_attribute_dict(model, exclude_readonly=True)
+ expected_filtered = {
+ "normal_field": "normal",
+ "different_wire": "different",
+ "keys_property": "keys_val",
+ "values_property": "values_val",
+ # "items_property" excluded because it's readonly
+ }
+ assert filtered_dict == expected_filtered
+
+ # Verify wire representations use correct wire names
+ wire_dict = dict(model)
+ expected_wire = {
+ "normal_field": "normal", # same as attr
+ "wireNameDifferent": "different", # different wire name
+ "keys_property": "keys_val", # same as attr (padded)
+ "valuesWire": "values_val", # different wire name
+ "itemsWire": "items_val", # different wire name
+ }
+ assert wire_dict == expected_wire
+
+ def test_no_backcompat_fallback(self):
+ """Test fallback behavior when no backcompat mapping exists"""
+
+ class NoBackcompatModel(HybridModel):
+ padded_attr: str = rest_field(name="wireField")
+ # Note: No original_tsp_name set, so no backcompat should occur
+
+ model = NoBackcompatModel(padded_attr="value")
+
+ # Should fall back to using actual attribute names
+ assert attribute_list(model) == ["padded_attr"]
+ assert as_attribute_dict(model) == {"padded_attr": "value"}
+ assert dict(model) == {"wireField": "value"}
+
+ def test_property_with_padding_in_actual_name(self):
+ """Test handling of properties that have padding in their actual
attribute names"""
+
+ class PaddingInNameModel(HybridModel):
+ keys_property: str = rest_field(name="myKeys")
+
+ model = PaddingInNameModel(keys_property="value")
+ # Should use actual attribute name since no original_tsp_name is set
+ assert attribute_list(model) == ["keys_property"]
+ assert as_attribute_dict(model) == {"keys_property": "value"}
+ assert dict(model) == {"myKeys": "value"}
+ assert getattr(model, "keys_property") == "value"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/azure_core-1.36.0/tests/test_stream_generator.py
new/azure_core-1.37.0/tests/test_stream_generator.py
--- old/azure_core-1.36.0/tests/test_stream_generator.py 2025-10-15
01:34:08.000000000 +0200
+++ new/azure_core-1.37.0/tests/test_stream_generator.py 2025-12-11
19:57:42.000000000 +0100
@@ -9,6 +9,7 @@
)
from azure.core.pipeline import Pipeline, PipelineResponse
from azure.core.pipeline.transport._requests_basic import
StreamDownloadGenerator
+from azure.core.exceptions import ServiceResponseError
try:
from unittest import mock
@@ -73,7 +74,7 @@
http_response.internal_response = MockInternalResponse()
stream = StreamDownloadGenerator(pipeline, http_response, decompress=False)
with mock.patch("time.sleep", return_value=None):
- with pytest.raises(requests.exceptions.ConnectionError):
+ with pytest.raises(ServiceResponseError):
stream.__next__()
@@ -133,5 +134,5 @@
pipeline = Pipeline(transport)
pipeline.run = mock_run
downloader = response.stream_download(pipeline, decompress=False)
- with pytest.raises(requests.exceptions.ConnectionError):
+ with pytest.raises(ServiceResponseError):
full_response = b"".join(downloader)