Skip to content
Merged
60 changes: 0 additions & 60 deletions azure-quantum/azure/quantum/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
from typing import Any, Dict
from azure.core import exceptions
from azure.storage.blob import (
BlobServiceClient,
ContainerClient,
BlobClient,
BlobSasPermissions,
ContentSettings,
generate_blob_sas,
generate_container_sas,
BlobType,
BlobProperties
)
Expand All @@ -21,64 +19,6 @@

logger = logging.getLogger(__name__)


def create_container(
connection_string: str, container_name: str
) -> ContainerClient:
"""
Creates and initialize a container; returns the client needed to access it.
"""
blob_service_client = BlobServiceClient.from_connection_string(
connection_string
)
logger.info(
f'{"Initializing storage client for account:"}'
+ f"{blob_service_client.account_name}"
)

container_client = blob_service_client.get_container_client(container_name)
create_container_using_client(container_client)
return container_client


def create_container_using_client(container_client: ContainerClient):
"""
Creates the container if it doesn't already exist.
"""
if not container_client.exists():
logger.debug(
f'{" - uploading to **new** container:"}'
f"{container_client.container_name}"
)
container_client.create_container()


def get_container_uri(connection_string: str, container_name: str) -> str:
"""
Creates and initialize a container;
returns a URI with a SAS read/write token to access it.
"""
container = create_container(connection_string, container_name)
logger.info(
f'{"Creating SAS token for container"}'
+ f"'{container_name}' on account: '{container.account_name}'"
)

sas_token = generate_container_sas(
container.account_name,
container.container_name,
account_key=container.credential.account_key,
permission=BlobSasPermissions(
read=True, add=True, write=True, create=True
),
expiry=datetime.utcnow() + timedelta(days=14),
)

uri = container.url + "?" + sas_token
logger.debug(f" - container url: '{uri}'.")
return uri


def upload_blob(
container: ContainerClient,
blob_name: str,
Expand Down
42 changes: 5 additions & 37 deletions azure-quantum/azure/quantum/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@
from azure.quantum._constants import (
ConnectionConstants,
)
from azure.quantum.storage import (
get_container_uri,
)
from azure.quantum._mgmt_client import WorkspaceMgmtClient
if TYPE_CHECKING:
from azure.quantum.target import Target
Expand All @@ -75,8 +72,7 @@ class Workspace:
to an Azure Quantum Workspace 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.
An Azure Quantum workspace only supports its linked storage.

:param subscription_id:
The Azure subscription ID.
Expand All @@ -90,11 +86,6 @@ class Workspace:
The Azure Quantum workspace name.
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.

:param resource_id:
The resource ID of the Azure Quantum workspace.

Expand Down Expand Up @@ -126,7 +117,7 @@ def __init__(
subscription_id: Optional[str] = None,
resource_group: Optional[str] = None,
name: Optional[str] = None,
storage: Optional[str] = None,
*,
resource_id: Optional[str] = None,
location: Optional[str] = None,
credential: Optional[object] = None,
Expand Down Expand Up @@ -162,7 +153,6 @@ def __init__(
connection_params.on_new_client_request = self._on_new_client_request

self._connection_params = connection_params
self._storage = storage

if not self._mgmt_client:
credential = connection_params.get_credential_or_default()
Expand Down Expand Up @@ -253,16 +243,6 @@ def credential(self) -> Any:
"""
return self._connection_params.credential

@property
def storage(self) -> str:
"""
Returns the Azure Storage account name associated with the Quantum Workspace.

:return: Azure Storage account name.
:rtype: str
"""
return self._storage

def _create_client(self) -> WorkspaceClient:
""""
An internal method to (re)create the underlying Azure SDK REST API client.
Expand Down Expand Up @@ -1029,22 +1009,10 @@ def get_container_uri(
if container_name is None:
if job_id is not None:
container_name = container_name_format.format(job_id=job_id)
elif job_id is None:
else:
container_name = f"{self.name}-data"
# Create container URI and get container client
if self.storage is None:
# Get linked storage account from the service, a new container
# is created by the service if it does not yet exist
container_uri = self._get_linked_storage_sas_uri(
container_name
)
else:
# Use the storage acount specified to generate container URI,
# create a new container if it does not yet exist
container_uri = get_container_uri(
self.storage, container_name
)
return container_uri

return self._get_linked_storage_sas_uri(container_name)

def _create_filter(self,
job_name: Optional[str] = None,
Expand Down
1 change: 0 additions & 1 deletion azure-quantum/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
RESOURCE_GROUP = "myresourcegroup"
WORKSPACE = "myworkspace"
LOCATION = "eastus"
STORAGE = "mystorage"
ENDPOINT_URI = f"https://{WORKSPACE}.{LOCATION}.quantum.azure.com/"
API_KEY = "myapikey"
75 changes: 35 additions & 40 deletions azure-quantum/tests/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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 import Priority, Workspace
from azure.quantum._constants import EnvironmentVariables, ConnectionConstants
from azure.core.credentials import AzureKeyCredential
from azure.core.pipeline.policies import AzureKeyCredentialPolicy
Expand All @@ -20,7 +20,6 @@
RESOURCE_GROUP,
WORKSPACE,
LOCATION,
STORAGE,
API_KEY,
ENDPOINT_URI,
)
Expand Down Expand Up @@ -63,38 +62,15 @@ 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,
)
assert_all_required_params(ws)
assert ws.storage == STORAGE

ws = WorkspaceMock(
resource_id=SIMPLE_RESOURCE_ID,
)
assert_all_required_params(ws)

ws = WorkspaceMock(
resource_id=SIMPLE_RESOURCE_ID,
storage=STORAGE,
)
assert_all_required_params(ws)
assert ws.storage == STORAGE

ws = WorkspaceMock(
name=WORKSPACE,
)
assert_all_required_params(ws)

ws = WorkspaceMock(
name=WORKSPACE,
storage=STORAGE,
)
assert_all_required_params(ws)
assert ws.storage == STORAGE

ws = WorkspaceMock(
name=WORKSPACE,
Expand Down Expand Up @@ -693,24 +669,43 @@ def test_workspace_context_manager_calls_enter_exit():
ws._mgmt_client.__exit__.assert_called_once()


def test_get_container_uri_uses_linked_storage_sas_when_storage_none():
"""When storage is None, get_container_uri should use linked storage via service SAS."""
def test_get_container_uri_uses_linked_storage_sas():
"""Use workspace-linked storage to obtain the container SAS URI."""
ws = WorkspaceMock(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
)
assert ws.storage is None

with mock.patch(
"azure.quantum.storage.ContainerClient.from_container_url",
return_value=mock.MagicMock(),
):
with mock.patch(
"azure.quantum.storage.create_container_using_client",
return_value=None,
):
uri = ws.get_container_uri(job_id="job-123")
assert isinstance(uri, str)
assert "https://example.com/" in uri
assert "sas-token" in uri
storage = ws._client.services.storage

with mock.patch.object(
storage,
"get_sas_uri",
return_value=mock.MagicMock(
sas_uri="https://example.com/job-job-123?sas-token"
),
) as get_sas_uri:
uri = Workspace.get_container_uri(ws, job_id="job-123")

get_sas_uri.assert_called_once()
args, kwargs = get_sas_uri.call_args
assert args == (SUBSCRIPTION_ID, RESOURCE_GROUP, WORKSPACE)
assert kwargs["blob_details"].container_name == "job-job-123"
assert kwargs["blob_details"].blob_name is None
assert uri == "https://example.com/job-job-123?sas-token"


def test_workspace_rejects_fourth_positional_argument():
with pytest.raises(TypeError):
Workspace(SUBSCRIPTION_ID, RESOURCE_GROUP, WORKSPACE, "legacy-storage")


def test_workspace_rejects_storage_keyword():
with pytest.raises(TypeError, match="storage"):
Workspace(
subscription_id=SUBSCRIPTION_ID,
resource_group=RESOURCE_GROUP,
name=WORKSPACE,
storage="legacy-storage",
)
Loading