Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions sdk/storage/azure-storage-blob/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
## 12.30.0b1 (Unreleased)

### Features Added
- Added support for service version 2026-06-06.
- Added support for connection strings and `account_url`s to accept URLs with `-ipv6` and `-dualstack` suffixes
for `BlobServiceClient`, `ContainerClient`, and `BlobClient`.
- Added support for `create` permission in `BlobSasPermissions` for `stage_block`,
`stage_block_from_url`, and `commit_block_list`.
- Added support for a new `Smart` access tier to `StandardBlobTier` used in `BlobClient.set_standard_blob_tier`,
which is optimized to automatically determine the most cost-effective access with no performance impact.
When set, `BlobProperties.smart_access_tier` will reveal the service's current access
tier choice between `Hot`, `Cool`, and `Archive`.

### Other Changes
- Consolidated the behavior of `max_concurrency=None` by defaulting to the shared `DEFAULT_MAX_CONCURRENCY` constant.

## 12.29.0b1 (2026-01-27)

Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azure-storage-blob/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/storage/azure-storage-blob",
"Tag": "python/storage/azure-storage-blob_7f2edbb0dc"
"Tag": "python/storage/azure-storage-blob_054396a10c"
}
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ def content_as_bytes(self, max_concurrency=None):

This method is deprecated, use func:`readall` instead.

:param int max_concurrency:
:param Optional[int] max_concurrency:
The number of parallel connections with which to download.
:return: The contents of the file as bytes.
:rtype: bytes
Expand All @@ -896,7 +896,7 @@ def content_as_text(self, max_concurrency=None, encoding="UTF-8"):

This method is deprecated, use func:`readall` instead.

:param int max_concurrency:
:param Optional[int] max_concurrency:
The number of parallel connections with which to download.
:param str encoding:
Test encoding to decode the downloaded bytes. Default is UTF-8.
Expand Down Expand Up @@ -924,7 +924,7 @@ def download_to_stream(self, stream, max_concurrency=None):
The stream to download to. This can be an open file-handle,
or any writable stream. The stream must be seekable if the download
uses more than one parallel connection.
:param int max_concurrency:
:param Optional[int] max_concurrency:
The number of parallel connections with which to download.
:return: The properties of the downloaded blob.
:rtype: Any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@
"file": {"primary": "FILEENDPOINT", "secondary": "FILESECONDARYENDPOINT"},
"dfs": {"primary": "BLOBENDPOINT", "secondary": "BLOBENDPOINT"},
}
_ACCOUNT_NAME_SUFFIXES = (
"-secondary-dualstack",
"-secondary-ipv6",
"-secondary",
"-dualstack",
"-ipv6",
)


def _strip_account_name_suffix(account_name: str) -> str:
"""Strip any well-known storage endpoint suffix from `account_name`.

Azure Storage endpoints may include suffixes such as `-secondary`,
`-dualstack` or `-ipv6` after the real account name. This function
removes those suffixes so callers always get back the base account name.

:param account_name: The raw account name segment extracted from a storage
endpoint hostname (i.e. everything before the first `.blob.core.`).
:type account_name: str
:return: The account name with any recognized suffix removed.
:rtype: str
"""
for suffix in _ACCOUNT_NAME_SUFFIXES:
if account_name.endswith(suffix):
return account_name[: -len(suffix)]
return account_name


class StorageAccountHostsMixin(object):
Expand Down Expand Up @@ -106,7 +132,7 @@ def __init__(
service_name = service.split("-")[0]
account = parsed_url.netloc.split(f".{service_name}.core.")

self.account_name = account[0] if len(account) > 1 else None
self.account_name = _strip_account_name_suffix(account[0]) if len(account) > 1 else None
if (
not self.account_name
and parsed_url.netloc.startswith("localhost")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ async def content_as_bytes(self, max_concurrency=None):

This method is deprecated, use func:`readall` instead.

:param int max_concurrency:
:param Optional[int] max_concurrency:
The number of parallel connections with which to download.
:return: The contents of the file as bytes.
:rtype: bytes
Expand All @@ -839,7 +839,7 @@ async def content_as_text(self, max_concurrency=None, encoding="UTF-8"):

This method is deprecated, use func:`readall` instead.

:param int max_concurrency:
:param Optional[int] max_concurrency:
The number of parallel connections with which to download.
:param str encoding:
Test encoding to decode the downloaded bytes. Default is UTF-8.
Expand Down Expand Up @@ -867,7 +867,7 @@ async def download_to_stream(self, stream, max_concurrency=None):
The stream to download to. This can be an open file-handle,
or any writable stream. The stream must be seekable if the download
uses more than one parallel connection.
:param int max_concurrency:
:param Optional[int] max_concurrency:
The number of parallel connections with which to download.
:return: The properties of the downloaded blob.
:rtype: Any
Expand Down
28 changes: 28 additions & 0 deletions sdk/storage/azure-storage-blob/tests/test_blob_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,34 @@ def test_create_service_with_socket_timeout(self, **kwargs):
assert service._client._client._pipeline._transport.connection_config.timeout == 22
assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)]

@pytest.mark.parametrize(
"account_url", [
"https://my-account.blob.core.windows.net/",
"https://my-account-secondary.blob.core.windows.net/",
"https://my-account-dualstack.blob.core.windows.net/",
"https://my-account-ipv6.blob.core.windows.net/",
"https://my-account-secondary-dualstack.blob.core.windows.net/",
"https://my-account-secondary-ipv6.blob.core.windows.net/",
]
)
@BlobPreparer()
def test_create_service_ipv6(self, account_url, **kwargs):
storage_account_name = "my-account"
storage_account_key = kwargs.pop("storage_account_key")

for service_type in SERVICES.keys():
service = service_type(
account_url,
credential=storage_account_key.secret,
container_name='foo',
blob_name='bar'
)

assert service is not None
assert service.scheme == "https"
assert service.account_name == storage_account_name
assert service.credential.account_key == storage_account_key.secret

# --Connection String Test Cases --------------------------------------------

@BlobPreparer()
Expand Down
28 changes: 28 additions & 0 deletions sdk/storage/azure-storage-blob/tests/test_blob_client_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,34 @@ def test_create_service_with_socket_timeout(self, **kwargs):
assert service._client._client._pipeline._transport.connection_config.timeout == 22
assert default_service._client._client._pipeline._transport.connection_config.timeout in [20, (20, 2000)]

@pytest.mark.parametrize(
"account_url", [
"https://my-account.blob.core.windows.net/",
"https://my-account-secondary.blob.core.windows.net/",
"https://my-account-dualstack.blob.core.windows.net/",
"https://my-account-ipv6.blob.core.windows.net/",
"https://my-account-secondary-dualstack.blob.core.windows.net/",
"https://my-account-secondary-ipv6.blob.core.windows.net/",
]
)
@BlobPreparer()
def test_create_service_ipv6(self, account_url, **kwargs):
storage_account_name = "my-account"
storage_account_key = kwargs.pop("storage_account_key")

for service_type in SERVICES.keys():
service = service_type(
account_url,
credential=storage_account_key.secret,
container_name='foo',
blob_name='bar'
)

assert service is not None
assert service.scheme == "https"
assert service.account_name == storage_account_name
assert service.credential.account_key == storage_account_key.secret

# --Connection String Test Cases --------------------------------------------
@BlobPreparer()
def test_create_service_with_connection_string_key(self, **kwargs):
Expand Down
8 changes: 8 additions & 0 deletions sdk/storage/azure-storage-file-datalake/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
## 12.25.0b1 (Unreleased)

### Features Added
- Added support for service version 2026-06-06.
- Added support for connection strings and `account_url`s to accept URLs with `-ipv6` and `-dualstack` suffixes
for `DataLakeServiceClient`, `FileSystemClient`, `DataLakeDirectoryClient`, and `DataLakeFileClient`.
- Added support for `DataLakeDirectoryClient` and `DataLakeFileClient`'s `set_tags` and `get_tags` APIs
to conditionally set and get tags associated with a directory or file client, respectively.

### Other Changes
- Consolidated the behavior of `max_concurrency=None` by defaulting to the shared `DEFAULT_MAX_CONCURRENCY` constant.

## 12.24.0b1 (2026-01-27)

Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azure-storage-file-datalake/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/storage/azure-storage-file-datalake",
"Tag": "python/storage/azure-storage-file-datalake_3d29de0db8"
"Tag": "python/storage/azure-storage-file-datalake_c0870501f2"
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ class PathClient(StorageAccountHostsMixin):
**kwargs: Any
) -> DataLakeLeaseClient: ...
@distributed_trace
def set_blob_tags(
def set_tags(
self,
tags: Optional[Dict[str, str]] = None,
*,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@
"file": {"primary": "FILEENDPOINT", "secondary": "FILESECONDARYENDPOINT"},
"dfs": {"primary": "BLOBENDPOINT", "secondary": "BLOBENDPOINT"},
}
_ACCOUNT_NAME_SUFFIXES = (
"-secondary-dualstack",
"-secondary-ipv6",
"-secondary",
"-dualstack",
"-ipv6",
)


def _strip_account_name_suffix(account_name: str) -> str:
"""Strip any well-known storage endpoint suffix from `account_name`.

Azure Storage endpoints may include suffixes such as `-secondary`,
`-dualstack` or `-ipv6` after the real account name. This function
removes those suffixes so callers always get back the base account name.

:param account_name: The raw account name segment extracted from a storage
endpoint hostname (i.e. everything before the first `.blob.core.`).
:type account_name: str
:return: The account name with any recognized suffix removed.
:rtype: str
"""
for suffix in _ACCOUNT_NAME_SUFFIXES:
if account_name.endswith(suffix):
return account_name[: -len(suffix)]
return account_name


class StorageAccountHostsMixin(object):
Expand Down Expand Up @@ -106,7 +132,7 @@ def __init__(
service_name = service.split("-")[0]
account = parsed_url.netloc.split(f".{service_name}.core.")

self.account_name = account[0] if len(account) > 1 else None
self.account_name = _strip_account_name_suffix(account[0]) if len(account) > 1 else None
if (
not self.account_name
and parsed_url.netloc.startswith("localhost")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class PathClient(AsyncStorageAccountHostsMixin, StorageAccountHostsMixin): # ty
**kwargs: Any
) -> DataLakeLeaseClient: ...
@distributed_trace_async
async def set_blob_tags(
async def set_tags(
self,
tags: Optional[Dict[str, str]] = None,
*,
Expand Down
54 changes: 54 additions & 0 deletions sdk/storage/azure-storage-file-datalake/tests/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# license information.
# --------------------------------------------------------------------------
import tempfile
import time
import unittest
from datetime import datetime, timedelta
from math import ceil
Expand Down Expand Up @@ -1875,6 +1876,59 @@ def callback(request):
props = identity_file.get_file_properties(raw_request_hook=callback)
assert props is not None

@DataLakePreparer()
@recorded_by_proxy
def test_data_lake_tags(self, **kwargs):
datalake_storage_account_name = kwargs.pop("datalake_storage_account_name")
datalake_storage_account_key = kwargs.pop("datalake_storage_account_key")

self._setUp(datalake_storage_account_name, datalake_storage_account_key)
directory_name = self._get_directory_reference()
self._create_directory_and_return_client(directory_name)
file_name = self._get_file_reference()
file_client = self.dsc.get_file_client(self.file_system_name, directory_name + '/' + file_name)
first_resp = file_client.create_file()

early = file_client.get_file_properties().last_modified
first_tags = {"tag1": "firsttag", "tag2": "secondtag", "tag3": "thirdtag"}
second_tags = {"tag4": "fourthtag", "tag5": "fifthtag", "tag6": "sixthtag"}

if self.is_live:
time.sleep(10)

with pytest.raises(ResourceModifiedError):
file_client.set_tags(first_tags, if_modified_since=early)
with pytest.raises(ResourceModifiedError):
file_client.get_tags(if_modified_since=early)
with pytest.raises(ResourceModifiedError):
file_client.set_tags(first_tags, etag=first_resp['etag'], match_condition=MatchConditions.IfModified)

file_client.set_tags(first_tags, if_unmodified_since=early)
tags = file_client.get_tags(if_unmodified_since=early)
assert tags == first_tags

file_client.set_tags(second_tags, etag=first_resp['etag'], match_condition=MatchConditions.IfNotModified)
tags = file_client.get_tags(etag=first_resp['etag'], match_condition=MatchConditions.IfNotModified)
assert tags == second_tags

data = b"abc123"
file_client.upload_data(data, length=len(data), overwrite=True)

with pytest.raises(ResourceModifiedError):
file_client.set_tags(first_tags, if_unmodified_since=early)
with pytest.raises(ResourceModifiedError):
file_client.get_tags(if_unmodified_since=early)
with pytest.raises(ResourceModifiedError):
file_client.set_tags(first_tags, etag=first_resp['etag'], match_condition=MatchConditions.IfNotModified)

file_client.set_tags(first_tags, if_modified_since=early)
tags = file_client.get_tags(if_modified_since=early)
assert tags == first_tags

file_client.set_tags(second_tags, etag=first_resp['etag'], match_condition=MatchConditions.IfModified)
tags = file_client.get_tags(etag=first_resp['etag'], match_condition=MatchConditions.IfModified)
assert tags == second_tags

# ------------------------------------------------------------------------------
if __name__ == '__main__':
unittest.main()
Loading
Loading