From c39edf0245e1c1dad61291bb9fe9bfee057c90a7 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Tue, 17 Mar 2026 23:57:04 -0400 Subject: [PATCH 1/2] WIP --- sdk/storage/azure-storage-blob/tests/test_blob_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client.py b/sdk/storage/azure-storage-blob/tests/test_blob_client.py index 5ad959336629..93f52187ccbe 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client.py @@ -299,6 +299,13 @@ 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)] + @BlobPreparer() + def test_create_service_ipv6(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + storage_account_key = kwargs.pop("storage_account_key") + + + # --Connection String Test Cases -------------------------------------------- @BlobPreparer() From 14d7c2aa9e9b313826d6ceca3228c597954b32bb Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Wed, 18 Mar 2026 01:46:08 -0400 Subject: [PATCH 2/2] [Storage] [STG 102] IPv6 Account Parsing --- .../azure/storage/blob/_shared/base_client.py | 28 +++++++++++++++++- .../tests/test_blob_client.py | 27 +++++++++++++++-- .../tests/test_blob_client_async.py | 28 ++++++++++++++++++ .../filedatalake/_shared/base_client.py | 28 +++++++++++++++++- .../storage/fileshare/_shared/base_client.py | 28 +++++++++++++++++- .../tests/test_file_client.py | 29 +++++++++++++++++++ .../tests/test_file_client_async.py | 29 +++++++++++++++++++ .../storage/queue/_shared/base_client.py | 28 +++++++++++++++++- .../tests/test_queue_client.py | 24 +++++++++++++++ .../tests/test_queue_client_async.py | 24 +++++++++++++++ 10 files changed, 266 insertions(+), 7 deletions(-) diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py index 5441488d86a9..33639019dfa8 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/base_client.py @@ -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): @@ -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") diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client.py b/sdk/storage/azure-storage-blob/tests/test_blob_client.py index 93f52187ccbe..c853c5364bf7 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client.py @@ -299,12 +299,33 @@ 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)] - @BlobPreparer() - def test_create_service_ipv6(self, **kwargs): - storage_account_name = kwargs.pop("storage_account_name") + @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 -------------------------------------------- diff --git a/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py b/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py index 3873f8d00b27..f91a26f87503 100644 --- a/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_blob_client_async.py @@ -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): diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py index 5441488d86a9..33639019dfa8 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/base_client.py @@ -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): @@ -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") diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py index 5441488d86a9..33639019dfa8 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/base_client.py @@ -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): @@ -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") diff --git a/sdk/storage/azure-storage-file-share/tests/test_file_client.py b/sdk/storage/azure-storage-file-share/tests/test_file_client.py index df8a607f27f8..62b01359e79a 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file_client.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file_client.py @@ -176,6 +176,35 @@ 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.file.core.windows.net/", + "https://my-account-secondary.file.core.windows.net/", + "https://my-account-dualstack.file.core.windows.net/", + "https://my-account-ipv6.file.core.windows.net/", + "https://my-account-secondary-dualstack.file.core.windows.net/", + "https://my-account-secondary-ipv6.file.core.windows.net/", + ] + ) + @FileSharePreparer() + 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, + share_name='foo', + directory_path='bar', + file_path='baz' + ) + + 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 -------------------------------------------- @FileSharePreparer() diff --git a/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py b/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py index bd2dac98405b..48c997780860 100644 --- a/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py +++ b/sdk/storage/azure-storage-file-share/tests/test_file_client_async.py @@ -176,6 +176,35 @@ async 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.file.core.windows.net/", + "https://my-account-secondary.file.core.windows.net/", + "https://my-account-dualstack.file.core.windows.net/", + "https://my-account-ipv6.file.core.windows.net/", + "https://my-account-secondary-dualstack.file.core.windows.net/", + "https://my-account-secondary-ipv6.file.core.windows.net/", + ] + ) + @FileSharePreparer() + 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, + share_name='foo', + directory_path='bar', + file_path='baz' + ) + + 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 -------------------------------------------- @FileSharePreparer() diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py index 3f7609b9f026..c3ca04b8f5f0 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/base_client.py @@ -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): @@ -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") diff --git a/sdk/storage/azure-storage-queue/tests/test_queue_client.py b/sdk/storage/azure-storage-queue/tests/test_queue_client.py index eefb88c6ce13..b3aba069fe92 100644 --- a/sdk/storage/azure-storage-queue/tests/test_queue_client.py +++ b/sdk/storage/azure-storage-queue/tests/test_queue_client.py @@ -235,6 +235,30 @@ 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.queue.core.windows.net/", + "https://my-account-secondary.queue.core.windows.net/", + "https://my-account-dualstack.queue.core.windows.net/", + "https://my-account-ipv6.queue.core.windows.net/", + "https://my-account-secondary-dualstack.queue.core.windows.net/", + "https://my-account-secondary-ipv6.queue.core.windows.net/", + ], + ) + @QueuePreparer() + 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, queue_name="foo") + + 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 -------------------------------------------- @QueuePreparer() def test_create_service_with_connection_string_key(self, **kwargs): diff --git a/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py b/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py index 1a2366e8dc41..2381a3cbfe10 100644 --- a/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py +++ b/sdk/storage/azure-storage-queue/tests/test_queue_client_async.py @@ -226,6 +226,30 @@ 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.queue.core.windows.net/", + "https://my-account-secondary.queue.core.windows.net/", + "https://my-account-dualstack.queue.core.windows.net/", + "https://my-account-ipv6.queue.core.windows.net/", + "https://my-account-secondary-dualstack.queue.core.windows.net/", + "https://my-account-secondary-ipv6.queue.core.windows.net/", + ], + ) + @QueuePreparer() + 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, queue_name="foo") + + 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 -------------------------------------------- @QueuePreparer() def test_create_service_with_connection_string_key(self, **kwargs):