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
44 changes: 31 additions & 13 deletions azure-quantum/azure/quantum/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations
from datetime import datetime
import logging
import warnings
from urllib.parse import quote
from typing import (
Any,
Expand Down Expand Up @@ -71,12 +72,12 @@ class Workspace:
2. specify a valid subscription ID, resource group, and workspace name; or
3. specify a valid workspace name.

You can also use a connection string to specify the connection parameters
to an Azure Quantum Workspace by calling
You can also use an Azure Quantum workspace authentication connection
string to specify the connection parameters by calling
:obj:`~ Workspace.from_connection_string() <Workspace.from_connection_string>`.

If the Azure Quantum workspace does not have linked storage, the caller
must also pass a valid Azure storage account connection string.
Azure Quantum workspaces require linked storage. Configure linked storage
on the workspace instead of passing a storage account connection string.

:param subscription_id:
The Azure subscription ID.
Expand All @@ -91,9 +92,9 @@ class Workspace:
Ignored if resource_id is specified.

:param storage:
The Azure storage account connection string.
Required only if the specified Azure Quantum
workspace does not have linked storage.
Deprecated. The Azure storage account connection string.
Configure workspace-linked storage instead. This parameter will be
removed in azure-quantum 4.0.0.

:param resource_id:
The resource ID of the Azure Quantum workspace.
Expand All @@ -120,6 +121,10 @@ class Workspace:
_QUANTUM_ENDPOINT_PARAM = '_quantum_endpoint'
_WORKSPACE_KIND_PARAM = '_workspace_kind'
_MGMT_CLIENT_PARAM = '_mgmt_client'
_STORAGE_DEPRECATION_MESSAGE = (
"Workspace(storage=...) is deprecated and will be removed in "
"azure-quantum 4.0.0. Configure workspace-linked storage instead."
)

def __init__(
self,
Expand All @@ -133,6 +138,13 @@ def __init__(
user_agent: Optional[str] = None,
**kwargs: Any,
) -> None:
if storage is not None:
warnings.warn(
Workspace._STORAGE_DEPRECATION_MESSAGE,
DeprecationWarning,
stacklevel=2,
)

# Extract internal params before passing kwargs to WorkspaceConnectionParams
# Param to track whether the workspace was created from a connection string
from_connection_string = kwargs.pop(Workspace._FROM_CONNECTION_STRING_PARAM, False)
Expand Down Expand Up @@ -254,12 +266,16 @@ def credential(self) -> Any:
return self._connection_params.credential

@property
def storage(self) -> str:
def storage(self) -> Optional[str]:
"""
Returns the Azure Storage account name associated with the Quantum Workspace.
Returns the explicitly configured Azure storage account connection string.

:return: Azure Storage account name.
:rtype: str
This property is retained for compatibility with the deprecated
``Workspace(storage=...)`` parameter. Configure workspace-linked storage
instead.

:return: Azure storage account connection string, or ``None`` if one was not configured.
:rtype: typing.Optional[str]
"""
return self._storage

Expand Down Expand Up @@ -312,10 +328,12 @@ def append_user_agent(self, value: str) -> None:
@classmethod
def from_connection_string(cls, connection_string: str, **kwargs) -> Workspace:
"""
Creates a new Azure Quantum Workspace client from a connection string.
Creates a new Azure Quantum Workspace client from an Azure Quantum
workspace authentication connection string.

:param connection_string:
A valid connection string, usually obtained from the
A valid Azure Quantum workspace authentication connection string,
distinct from a storage account connection string and usually obtained from the
`Quantum Workspace -> Operations -> Access Keys` blade in the Azure Portal.

:return: New Azure Quantum Workspace client.
Expand Down
101 changes: 87 additions & 14 deletions azure-quantum/tests/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
##

import pytest
import inspect
import os
import warnings
from unittest import mock
from azure.quantum.job.job import Job
from azure.quantum._client.models import JobDetails
from azure.quantum import Priority
from azure.quantum.workspace import Workspace
from azure.quantum._constants import EnvironmentVariables, ConnectionConstants
from azure.core.credentials import AzureKeyCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy
Expand Down Expand Up @@ -47,6 +50,11 @@
quantum_endpoint=ConnectionConstants.GET_QUANTUM_PRODUCTION_ENDPOINT_v2(LOCATION)
)

STORAGE_DEPRECATION_WARNING = (
"Workspace(storage=...) is deprecated and will be removed in "
"azure-quantum 4.0.0. Configure workspace-linked storage instead."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't be waiting 4.0.0, let's search usages of that storage parameter in public places and internally and decide if we can just remove it or we need deprecation period

)


def test_create_workspace_instance_valid():
def assert_all_required_params(ws: WorkspaceMock):
Expand All @@ -63,12 +71,13 @@ def assert_all_required_params(ws: WorkspaceMock):
)
assert_all_required_params(ws)

ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
storage=STORAGE,
)
with pytest.warns(DeprecationWarning):
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
storage=STORAGE,
)
assert_all_required_params(ws)
assert ws.storage == STORAGE

Expand All @@ -77,10 +86,11 @@ def assert_all_required_params(ws: WorkspaceMock):
)
assert_all_required_params(ws)

ws = WorkspaceMock(
resource_id=SIMPLE_RESOURCE_ID,
storage=STORAGE,
)
with pytest.warns(DeprecationWarning):
ws = WorkspaceMock(
resource_id=SIMPLE_RESOURCE_ID,
storage=STORAGE,
)
assert_all_required_params(ws)
assert ws.storage == STORAGE

Expand All @@ -89,10 +99,11 @@ def assert_all_required_params(ws: WorkspaceMock):
)
assert_all_required_params(ws)

ws = WorkspaceMock(
name=WORKSPACE,
storage=STORAGE,
)
with pytest.warns(DeprecationWarning):
ws = WorkspaceMock(
name=WORKSPACE,
storage=STORAGE,
)
assert_all_required_params(ws)
assert ws.storage == STORAGE

Expand Down Expand Up @@ -129,6 +140,48 @@ def assert_all_required_params(ws: WorkspaceMock):
assert_all_required_params(ws)


def test_workspace_storage_parameter_is_deprecated():
with pytest.warns(DeprecationWarning) as warning_info:
workspace = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
storage=STORAGE,
)

assert str(warning_info[0].message) == STORAGE_DEPRECATION_WARNING
source_lines, start_line = inspect.getsourcelines(WorkspaceMock.__init__)
caller_offset = next(
index for index, line in enumerate(source_lines)
if "super().__init__" in line
)
assert warning_info[0].filename == inspect.getsourcefile(WorkspaceMock.__init__)
assert warning_info[0].lineno == start_line + caller_offset
assert workspace.storage == STORAGE


@pytest.mark.parametrize("workspace_kwargs", [{}, {"storage": None}])
def test_workspace_without_storage_does_not_warn(workspace_kwargs):
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
workspace = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
**workspace_kwargs,
)

assert workspace.storage is None


def test_workspace_from_connection_string_does_not_warn_about_storage():
with mock.patch.dict(os.environ, clear=True), warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
workspace = WorkspaceMock.from_connection_string(SIMPLE_CONNECTION_STRING)

assert workspace.name == WORKSPACE


def test_create_workspace_locations():
# Location name should be normalized
_mgmt_client = MockWorkspaceMgmtClient()
Expand Down Expand Up @@ -714,3 +767,23 @@ def test_get_container_uri_uses_linked_storage_sas_when_storage_none():
assert isinstance(uri, str)
assert "https://example.com/" in uri
assert "sas-token" in uri


def test_get_container_uri_uses_explicit_storage_connection_string():
with pytest.warns(DeprecationWarning):
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
storage=STORAGE,
)

with mock.patch(
"azure.quantum.workspace.get_container_uri",
return_value="https://example.com/container?sas-token",
) as mock_get_container_uri:
# WorkspaceMock overrides get_container_uri for offline tests; call the real implementation directly.
uri = Workspace.get_container_uri(ws, job_id="job-123")

mock_get_container_uri.assert_called_once_with(STORAGE, "job-job-123")
assert uri == "https://example.com/container?sas-token"
Loading