Skip to content
Draft
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
9 changes: 9 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,15 @@
## 12.31.0b1 (Unreleased)

### Features Added
- Added support for service version 2026-10-06.
- Added access tier information to the response of `BlobClient`'s `download_blob` API.
The `blob_tier`, `blob_tier_inferred`, `blob_tier_change_time`, and `smart_access_tier`
properties are now populated on the downloaded blob's `properties`.
- The service-calculated CRC64 is now surfaced as `content_crc64` on the response of
`BlobClient` upload operations (`stage_block`, `stage_block_from_url`, `upload_page`,
`upload_pages_from_url`, `append_block`, `append_block_from_url`, `upload_blob`, and
`upload_blob_from_url`) in addition to `content_md5` when a content MD5 is
provided with the request.

## 12.30.0 (2026-06-08)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ async def download(
response_headers["x-ms-structured-content-length"] = self._deserialize(
"int", response.headers.get("x-ms-structured-content-length")
)
response_headers["x-ms-access-tier"] = self._deserialize("str", response.headers.get("x-ms-access-tier"))
response_headers["x-ms-access-tier-inferred"] = self._deserialize(
"bool", response.headers.get("x-ms-access-tier-inferred")
)
response_headers["x-ms-access-tier-change-time"] = self._deserialize(
"rfc-1123", response.headers.get("x-ms-access-tier-change-time")
)
response_headers["x-ms-smart-access-tier"] = self._deserialize(
"str", response.headers.get("x-ms-smart-access-tier")
)

if response.status_code == 206:
response_headers["Last-Modified"] = self._deserialize("rfc-1123", response.headers.get("Last-Modified"))
Expand Down Expand Up @@ -393,6 +403,16 @@ async def download(
response_headers["x-ms-structured-content-length"] = self._deserialize(
"int", response.headers.get("x-ms-structured-content-length")
)
response_headers["x-ms-access-tier"] = self._deserialize("str", response.headers.get("x-ms-access-tier"))
response_headers["x-ms-access-tier-inferred"] = self._deserialize(
"bool", response.headers.get("x-ms-access-tier-inferred")
)
response_headers["x-ms-access-tier-change-time"] = self._deserialize(
"rfc-1123", response.headers.get("x-ms-access-tier-change-time")
)
response_headers["x-ms-smart-access-tier"] = self._deserialize(
"str", response.headers.get("x-ms-smart-access-tier")
)

deserialized = response.stream_download(self._client._pipeline, decompress=_decompress)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ async def upload( # pylint: disable=too-many-locals
response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag"))
response_headers["Last-Modified"] = self._deserialize("rfc-1123", response.headers.get("Last-Modified"))
response_headers["Content-MD5"] = self._deserialize("bytearray", response.headers.get("Content-MD5"))
response_headers["x-ms-content-crc64"] = self._deserialize(
"bytearray", response.headers.get("x-ms-content-crc64")
)
response_headers["x-ms-client-request-id"] = self._deserialize(
"str", response.headers.get("x-ms-client-request-id")
)
Expand Down Expand Up @@ -522,6 +525,9 @@ async def put_blob_from_url( # pylint: disable=too-many-locals
response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag"))
response_headers["Last-Modified"] = self._deserialize("rfc-1123", response.headers.get("Last-Modified"))
response_headers["Content-MD5"] = self._deserialize("bytearray", response.headers.get("Content-MD5"))
response_headers["x-ms-content-crc64"] = self._deserialize(
"bytearray", response.headers.get("x-ms-content-crc64")
)
response_headers["x-ms-client-request-id"] = self._deserialize(
"str", response.headers.get("x-ms-client-request-id")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,16 @@ def download(
response_headers["x-ms-structured-content-length"] = self._deserialize(
"int", response.headers.get("x-ms-structured-content-length")
)
response_headers["x-ms-access-tier"] = self._deserialize("str", response.headers.get("x-ms-access-tier"))
response_headers["x-ms-access-tier-inferred"] = self._deserialize(
"bool", response.headers.get("x-ms-access-tier-inferred")
)
response_headers["x-ms-access-tier-change-time"] = self._deserialize(
"rfc-1123", response.headers.get("x-ms-access-tier-change-time")
)
response_headers["x-ms-smart-access-tier"] = self._deserialize(
"str", response.headers.get("x-ms-smart-access-tier")
)

if response.status_code == 206:
response_headers["Last-Modified"] = self._deserialize("rfc-1123", response.headers.get("Last-Modified"))
Expand Down Expand Up @@ -1863,6 +1873,16 @@ def download(
response_headers["x-ms-structured-content-length"] = self._deserialize(
"int", response.headers.get("x-ms-structured-content-length")
)
response_headers["x-ms-access-tier"] = self._deserialize("str", response.headers.get("x-ms-access-tier"))
response_headers["x-ms-access-tier-inferred"] = self._deserialize(
"bool", response.headers.get("x-ms-access-tier-inferred")
)
response_headers["x-ms-access-tier-change-time"] = self._deserialize(
"rfc-1123", response.headers.get("x-ms-access-tier-change-time")
)
response_headers["x-ms-smart-access-tier"] = self._deserialize(
"str", response.headers.get("x-ms-smart-access-tier")
)

deserialized = response.stream_download(self._client._pipeline, decompress=_decompress)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,9 @@ def upload( # pylint: disable=inconsistent-return-statements,too-many-locals
response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag"))
response_headers["Last-Modified"] = self._deserialize("rfc-1123", response.headers.get("Last-Modified"))
response_headers["Content-MD5"] = self._deserialize("bytearray", response.headers.get("Content-MD5"))
response_headers["x-ms-content-crc64"] = self._deserialize(
"bytearray", response.headers.get("x-ms-content-crc64")
)
response_headers["x-ms-client-request-id"] = self._deserialize(
"str", response.headers.get("x-ms-client-request-id")
)
Expand Down Expand Up @@ -1181,6 +1184,9 @@ def put_blob_from_url( # pylint: disable=inconsistent-return-statements,too-man
response_headers["ETag"] = self._deserialize("str", response.headers.get("ETag"))
response_headers["Last-Modified"] = self._deserialize("rfc-1123", response.headers.get("Last-Modified"))
response_headers["Content-MD5"] = self._deserialize("bytearray", response.headers.get("Content-MD5"))
response_headers["x-ms-content-crc64"] = self._deserialize(
"bytearray", response.headers.get("x-ms-content-crc64")
)
response_headers["x-ms-client-request-id"] = self._deserialize(
"str", response.headers.get("x-ms-client-request-id")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"2026-02-06",
"2026-04-06",
"2026-06-06",
"2026-10-06",
]


Expand Down
4 changes: 4 additions & 0 deletions sdk/storage/azure-storage-blob/tests/test_append_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ def test_append_block_with_md5(self, **kwargs):
assert resp["blob_committed_block_count"] == 1
assert resp["etag"] is not None
assert resp["last_modified"] is not None
assert resp["content_md5"] is not None
assert resp["content_crc64"] is not None

# Assert

Expand Down Expand Up @@ -410,6 +412,8 @@ def test_append_block_from_url_and_validate_content_md5(self, **kwargs):
assert resp.get("blob_committed_block_count") == 1
assert resp.get("etag") is not None
assert resp.get("last_modified") is not None
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

# Assert the destination blob is constructed correctly
destination_blob_properties = destination_blob_client.get_blob_properties()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ async def test_append_block_with_md5(self, **kwargs):
assert resp["blob_committed_block_count"] == 1
assert resp["etag"] is not None
assert resp["last_modified"] is not None
assert resp["content_md5"] is not None
assert resp["content_crc64"] is not None

# Assert

Expand Down Expand Up @@ -409,6 +411,8 @@ async def test_append_block_from_url_and_validate_content_md5(self, **kwargs):
assert resp.get("blob_committed_block_count") == 1
assert resp.get("etag") is not None
assert resp.get("last_modified") is not None
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

# Assert the destination blob is constructed correctly
destination_blob_properties = await destination_blob_client.get_blob_properties()
Expand Down
8 changes: 6 additions & 2 deletions sdk/storage/azure-storage-blob/tests/test_block_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,9 +585,11 @@ def test_upload_blob_from_url_with_source_content_md5(self, **kwargs):
new_blob = self.bsc.get_blob_client(self.container_name, blob_name)

# Assert
new_blob.upload_blob_from_url(
resp = new_blob.upload_blob_from_url(
source_blob_url, include_source_blob_properties=True, source_content_md5=source_md5
)
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None
with pytest.raises(HttpResponseError):
new_blob.upload_blob_from_url(
source_blob_url, include_source_blob_properties=False, source_content_md5=bad_source_md5
Expand Down Expand Up @@ -718,9 +720,11 @@ def test_put_block_with_md5(self, **kwargs):
blob = self._create_blob()

# Act
blob.stage_block(1, b"block", validate_content=True)
resp = blob.stage_block(1, b"block", validate_content=True)

# Assert
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

@BlobPreparer()
@recorded_by_proxy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,11 @@ async def test_upload_blob_from_url_with_source_content_md5(self, **kwargs):
new_blob = self.bsc.get_blob_client(self.container_name, blob_name)

# Assert
await new_blob.upload_blob_from_url(
resp = await new_blob.upload_blob_from_url(
source_blob_url, include_source_blob_properties=True, source_content_md5=source_md5
)
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None
with pytest.raises(HttpResponseError):
await new_blob.upload_blob_from_url(
source_blob_url, include_source_blob_properties=False, source_content_md5=bad_source_md5
Expand Down Expand Up @@ -803,9 +805,11 @@ async def test_put_block_with_md5(self, **kwargs):
blob = await self._create_blob()

# Act
await blob.stage_block(1, b"block", validate_content=True)
resp = await blob.stage_block(1, b"block", validate_content=True)

# Assert
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

@BlobPreparer()
@recorded_by_proxy_async
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,15 @@ def test_put_block_from_url_and_validate_content_md5(self, **kwargs):
src_md5 = calculate_content_md5(self.source_blob_data)

# Act part 1: put block from url with md5 validation
dest_blob.stage_block_from_url(
resp = dest_blob.stage_block_from_url(
block_id=1,
source_url=self.source_blob_url,
source_content_md5=src_md5,
source_offset=0,
source_length=8 * 1024,
)
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

# Assert block was staged
committed, uncommitted = dest_blob.get_block_list("all")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,15 @@ async def test_put_block_from_url_and_vldte_content_md5(self, **kwargs):
src_md5 = calculate_content_md5(self.source_blob_data)

# Act part 1: put block from url with md5 validation
await dest_blob.stage_block_from_url(
resp = await dest_blob.stage_block_from_url(
block_id=1,
source_url=self.source_blob_url,
source_content_md5=src_md5,
source_offset=0,
source_length=8 * 1024,
)
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

# Assert block was staged
committed, uncommitted = await dest_blob.get_block_list("all")
Expand Down
26 changes: 23 additions & 3 deletions sdk/storage/azure-storage-blob/tests/test_common_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ def test_set_blob_metadata_with_upper_case(self, **kwargs):
assert md["hello"] == "world"
assert md["number"] == "42"
assert md["UP"] == "UPval"
assert not "up" in md
assert "up" not in md

@BlobPreparer()
@recorded_by_proxy
Expand All @@ -995,7 +995,7 @@ def test_set_blob_metadata_with_if_tags(self, **kwargs):
assert md["hello"] == "world"
assert md["number"] == "42"
assert md["UP"] == "UPval"
assert not "up" in md
assert "up" not in md

@BlobPreparer()
@recorded_by_proxy
Expand All @@ -1018,7 +1018,7 @@ def test_set_blob_metadata_returns_vid(self, **kwargs):
assert md["hello"] == "world"
assert md["number"] == "42"
assert md["UP"] == "UPval"
assert not "up" in md
assert "up" not in md

@BlobPreparer()
@recorded_by_proxy
Expand Down Expand Up @@ -4000,4 +4000,24 @@ def test_blob_fns_directory_fail(self, **kwargs):

return variables

@BlobPreparer()
@recorded_by_proxy
def test_download_blob_returns_access_tier(self, **kwargs):
storage_account_name = kwargs.pop("storage_account_name")
storage_account_key = kwargs.pop("storage_account_key")

self._setup(storage_account_name, storage_account_key)
blob = self.bsc.get_container_client(self.container_name).get_blob_client(self._get_blob_reference())
blob.upload_blob(b"hello world", overwrite=True)
blob.set_standard_blob_tier(StandardBlobTier.SMART)

# Act
downloader = blob.download_blob()

# Assert
assert StandardBlobTier.SMART == downloader.properties.blob_tier
assert downloader.properties.smart_access_tier is not None
assert downloader.properties.blob_tier_change_time is not None
assert not downloader.properties.blob_tier_inferred

# ------------------------------------------------------------------------------
26 changes: 23 additions & 3 deletions sdk/storage/azure-storage-blob/tests/test_common_blob_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,7 @@ async def test_set_blob_metadata_with_upper_case(self, **kwargs):
assert md["hello"] == "world"
assert md["number"] == "42"
assert md["UP"] == "UPval"
assert not "up" in md
assert "up" not in md

@BlobPreparer()
@recorded_by_proxy_async
Expand Down Expand Up @@ -1238,7 +1238,7 @@ async def test_set_blob_metadata_with_if_tags(self, **kwargs):
assert md["hello"] == "world"
assert md["number"] == "42"
assert md["UP"] == "UPval"
assert not "up" in md
assert "up" not in md

@BlobPreparer()
@recorded_by_proxy_async
Expand All @@ -1262,7 +1262,7 @@ async def test_set_blob_metadata_returns_vid(self, **kwargs):
assert md["hello"] == "world"
assert md["number"] == "42"
assert md["UP"] == "UPval"
assert not "up" in md
assert "up" not in md

@BlobPreparer()
@recorded_by_proxy_async
Expand Down Expand Up @@ -3908,5 +3908,25 @@ async def test_blob_fns_directory_fail(self, **kwargs):

return variables

@BlobPreparer()
@recorded_by_proxy_async
async def test_download_blob_returns_access_tier(self, **kwargs):
storage_account_name = kwargs.pop("storage_account_name")
storage_account_key = kwargs.pop("storage_account_key")

await self._setup(storage_account_name, storage_account_key)
blob = self.bsc.get_container_client(self.container_name).get_blob_client(self._get_blob_reference())
await blob.upload_blob(b"hello world")
await blob.set_standard_blob_tier(StandardBlobTier.SMART)

# Act
downloader = await blob.download_blob()

# Assert
assert StandardBlobTier.SMART == downloader.properties.blob_tier
assert downloader.properties.smart_access_tier is not None
assert downloader.properties.blob_tier_change_time is not None
assert not downloader.properties.blob_tier_inferred


# ------------------------------------------------------------------------------
6 changes: 5 additions & 1 deletion sdk/storage/azure-storage-blob/tests/test_page_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,11 @@ def test_update_page_with_md5(self, **kwargs):

# Act
data = self.get_random_bytes(512)
blob.upload_page(data, offset=0, length=512, validate_content=True)
resp = blob.upload_page(data, offset=0, length=512, validate_content=True)

# Assert
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

@BlobPreparer()
@recorded_by_proxy
Expand Down Expand Up @@ -752,6 +754,8 @@ def test_upload_pages_from_url_and_validate_content_md5(self, **kwargs):
)
assert resp.get("etag") is not None
assert resp.get("last_modified") is not None
assert resp.get("content_md5") is not None
assert resp.get("content_crc64") is not None

# Assert the destination blob is constructed correctly
blob_properties = destination_blob_client.get_blob_properties()
Expand Down
Loading
Loading