azure-quantum 3.5.0.dev0__py3-none-any.whl → 3.5.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,4 +6,4 @@
6
6
  # Changes may cause incorrect behavior and will be lost if the code is regenerated.
7
7
  # --------------------------------------------------------------------------
8
8
 
9
- VERSION = "3.5.0.dev0"
9
+ VERSION = "3.5.1.dev1"
@@ -55,6 +55,9 @@ class ConnectionConstants:
55
55
  DATA_PLANE_CREDENTIAL_SCOPE = "https://quantum.microsoft.com/.default"
56
56
  ARM_CREDENTIAL_SCOPE = "https://management.azure.com/.default"
57
57
 
58
+ DEFAULT_ARG_API_VERSION = "2021-03-01"
59
+ DEFAULT_WORKSPACE_API_VERSION = "2025-11-01-preview"
60
+
58
61
  MSA_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad"
59
62
 
60
63
  AUTHORITY = AzureIdentityInternals.get_default_authority()
@@ -63,10 +66,14 @@ class ConnectionConstants:
63
66
  # pylint: disable=unnecessary-lambda-assignment
64
67
  GET_QUANTUM_PRODUCTION_ENDPOINT = \
65
68
  lambda location: f"https://{location}.quantum.azure.com/"
69
+ GET_QUANTUM_PRODUCTION_ENDPOINT_v2 = \
70
+ lambda location: f"https://{location}-v2.quantum.azure.com/"
66
71
  GET_QUANTUM_CANARY_ENDPOINT = \
67
72
  lambda location: f"https://{location or 'eastus2euap'}.quantum.azure.com/"
68
73
  GET_QUANTUM_DOGFOOD_ENDPOINT = \
69
74
  lambda location: f"https://{location}.quantum-test.azure.com/"
75
+ GET_QUANTUM_DOGFOOD_ENDPOINT_v2 = \
76
+ lambda location: f"https://{location}-v2.quantum-test.azure.com/"
70
77
 
71
78
  ARM_PRODUCTION_ENDPOINT = "https://management.azure.com/"
72
79
  ARM_DOGFOOD_ENDPOINT = "https://api-dogfood.resources.windows-int.net/"
@@ -93,3 +100,65 @@ class ConnectionConstants:
93
100
  GUID_REGEX_PATTERN = (
94
101
  r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
95
102
  )
103
+
104
+ VALID_WORKSPACE_NAME_PATTERN = r"^[a-zA-Z0-9]+(-*[a-zA-Z0-9])*$"
105
+
106
+ VALID_AZURE_REGIONS = {
107
+ "australiacentral",
108
+ "australiacentral2",
109
+ "australiaeast",
110
+ "australiasoutheast",
111
+ "austriaeast",
112
+ "belgiumcentral",
113
+ "brazilsouth",
114
+ "brazilsoutheast",
115
+ "canadacentral",
116
+ "canadaeast",
117
+ "centralindia",
118
+ "centralus",
119
+ "centraluseuap",
120
+ "chilecentral",
121
+ "eastasia",
122
+ "eastus",
123
+ "eastus2",
124
+ "eastus2euap",
125
+ "francecentral",
126
+ "francesouth",
127
+ "germanynorth",
128
+ "germanywestcentral",
129
+ "indonesiacentral",
130
+ "israelcentral",
131
+ "italynorth",
132
+ "japaneast",
133
+ "japanwest",
134
+ "koreacentral",
135
+ "koreasouth",
136
+ "malaysiawest",
137
+ "mexicocentral",
138
+ "newzealandnorth",
139
+ "northcentralus",
140
+ "northeurope",
141
+ "norwayeast",
142
+ "norwaywest",
143
+ "polandcentral",
144
+ "qatarcentral",
145
+ "southafricanorth",
146
+ "southafricawest",
147
+ "southcentralus",
148
+ "southindia",
149
+ "southeastasia",
150
+ "spaincentral",
151
+ "swedencentral",
152
+ "switzerlandnorth",
153
+ "switzerlandwest",
154
+ "uaecentral",
155
+ "uaenorth",
156
+ "uksouth",
157
+ "ukwest",
158
+ "westcentralus",
159
+ "westeurope",
160
+ "westindia",
161
+ "westus",
162
+ "westus2",
163
+ "westus3",
164
+ }
@@ -0,0 +1,239 @@
1
+ ##
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License.
4
+ ##
5
+ """
6
+ Module providing the WorkspaceMgmtClient class for managing workspace operations.
7
+ Created to do not add additional azure-mgmt-* dependencies that can conflict with existing ones.
8
+ """
9
+
10
+ import logging
11
+ from http import HTTPStatus
12
+ from typing import Any, Optional, cast
13
+ from azure.core import PipelineClient
14
+ from azure.core.credentials import TokenProvider
15
+ from azure.core.pipeline import policies
16
+ from azure.core.rest import HttpRequest
17
+ from azure.core.exceptions import HttpResponseError
18
+ from azure.quantum._workspace_connection_params import WorkspaceConnectionParams
19
+ from azure.quantum._constants import ConnectionConstants
20
+ from azure.quantum._client._configuration import VERSION
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ __all__ = ["WorkspaceMgmtClient"]
25
+
26
+
27
+ class WorkspaceMgmtClient():
28
+ """
29
+ Client for Azure Quantum Workspace related ARM/ARG operations.
30
+ Uses PipelineClient under the hood which is standard for all Azure SDK clients,
31
+ see https://learn.microsoft.com/en-us/azure/developer/python/sdk/fundamentals/http-pipeline-retries.
32
+
33
+ :param credential:
34
+ The credential to use to connect to Azure services.
35
+
36
+ :param base_url:
37
+ The base URL for the ARM endpoint.
38
+
39
+ :param user_agent:
40
+ Add the specified value as a prefix to the HTTP User-Agent header.
41
+ """
42
+
43
+ # Constants
44
+ DEFAULT_RETRY_TOTAL = 3
45
+ CONTENT_TYPE_JSON = "application/json"
46
+ CONNECT_DOC_LINK = "https://learn.microsoft.com/en-us/azure/quantum/how-to-connect-workspace"
47
+ CONNECT_DOC_MESSAGE = f"To find details on how to connect to your workspace, please see {CONNECT_DOC_LINK}."
48
+
49
+ def __init__(self, credential: TokenProvider, base_url: str, user_agent: Optional[str] = None) -> None:
50
+ """
51
+ Initialize the WorkspaceMgmtClient.
52
+
53
+ :param credential:
54
+ The credential to use to connect to Azure services.
55
+
56
+ :param base_url:
57
+ The base URL for the ARM endpoint.
58
+ """
59
+ self._credential = credential
60
+ self._base_url = base_url
61
+ self._policies = [
62
+ policies.RequestIdPolicy(),
63
+ policies.HeadersPolicy({
64
+ "Content-Type": self.CONTENT_TYPE_JSON,
65
+ "Accept": self.CONTENT_TYPE_JSON,
66
+ }),
67
+ policies.UserAgentPolicy(user_agent=user_agent, sdk_moniker="quantum/{}".format(VERSION)),
68
+ policies.RetryPolicy(retry_total=self.DEFAULT_RETRY_TOTAL),
69
+ policies.BearerTokenCredentialPolicy(self._credential, ConnectionConstants.ARM_CREDENTIAL_SCOPE),
70
+ ]
71
+ self._client: PipelineClient = PipelineClient(base_url=cast(str, base_url), policies=self._policies)
72
+
73
+ def close(self) -> None:
74
+ self._client.close()
75
+
76
+ def __enter__(self) -> 'WorkspaceMgmtClient':
77
+ self._client.__enter__()
78
+ return self
79
+
80
+ def __exit__(self, *exc_details: Any) -> None:
81
+ self._client.__exit__(*exc_details)
82
+
83
+ def load_workspace_from_arg(self, connection_params: WorkspaceConnectionParams) -> None:
84
+ """
85
+ Queries Azure Resource Graph to find a workspace by name and optionally location, resource group, subscription.
86
+ Provided workspace name, location, resource group, and subscription in connection params must be validated beforehand.
87
+
88
+ :param connection_params:
89
+ The workspace connection parameters to use and update.
90
+ """
91
+ if not connection_params.workspace_name:
92
+ raise ValueError("Workspace name must be specified to try to load workspace details from ARG.")
93
+
94
+ query = f"""
95
+ Resources
96
+ | where type =~ 'microsoft.quantum/workspaces'
97
+ | where name =~ '{connection_params.workspace_name}'
98
+ """
99
+
100
+ if connection_params.resource_group:
101
+ query += f"\n | where resourceGroup =~ '{connection_params.resource_group}'"
102
+
103
+ if connection_params.location:
104
+ query += f"\n | where location =~ '{connection_params.location}'"
105
+
106
+ query += """
107
+ | extend endpointUri = tostring(properties.endpointUri)
108
+ | project name, subscriptionId, resourceGroup, location, endpointUri
109
+ """
110
+
111
+ request_body = {
112
+ "query": query
113
+ }
114
+
115
+ if connection_params.subscription_id:
116
+ request_body["subscriptions"] = [connection_params.subscription_id]
117
+
118
+ # Create request to Azure Resource Graph API
119
+ request = HttpRequest(
120
+ method="POST",
121
+ url=self._client.format_url("/providers/Microsoft.ResourceGraph/resources"),
122
+ params={"api-version": ConnectionConstants.DEFAULT_ARG_API_VERSION},
123
+ json=request_body
124
+ )
125
+
126
+ try:
127
+ response = self._client.send_request(request)
128
+ response.raise_for_status()
129
+ result = response.json()
130
+ except Exception as e:
131
+ raise RuntimeError(
132
+ f"Could not load workspace details from Azure Resource Graph: {str(e)}.\n{self.CONNECT_DOC_MESSAGE}"
133
+ ) from e
134
+
135
+ data = result.get('data', [])
136
+
137
+ if not data:
138
+ raise ValueError(f"No matching workspace found with name '{connection_params.workspace_name}'. {self.CONNECT_DOC_MESSAGE}")
139
+
140
+ if len(data) > 1:
141
+ raise ValueError(
142
+ f"Multiple Azure Quantum workspaces found with name '{connection_params.workspace_name}'. "
143
+ f"Please specify additional connection parameters. {self.CONNECT_DOC_MESSAGE}"
144
+ )
145
+
146
+ workspace_data = data[0]
147
+
148
+ connection_params.subscription_id = workspace_data.get('subscriptionId')
149
+ connection_params.resource_group = workspace_data.get('resourceGroup')
150
+ connection_params.location = workspace_data.get('location')
151
+ connection_params.quantum_endpoint = workspace_data.get('endpointUri')
152
+
153
+ logger.debug(
154
+ "Found workspace '%s' in subscription '%s', resource group '%s', location '%s', endpoint '%s'",
155
+ connection_params.workspace_name,
156
+ connection_params.subscription_id,
157
+ connection_params.resource_group,
158
+ connection_params.location,
159
+ connection_params.quantum_endpoint
160
+ )
161
+
162
+ # If one of the required parameters is missing, probably workspace in failed provisioning state
163
+ if not connection_params.is_complete():
164
+ raise ValueError(
165
+ f"Failed to retrieve complete workspace details for workspace '{connection_params.workspace_name}'. "
166
+ "Please check that workspace is in valid state."
167
+ )
168
+
169
+ def load_workspace_from_arm(self, connection_params: WorkspaceConnectionParams) -> None:
170
+ """
171
+ Fetches the workspace resource from ARM and sets location and endpoint URI params.
172
+ Provided workspace name, resource group, and subscription in connection params must be validated beforehand.
173
+
174
+ :param connection_params:
175
+ The workspace connection parameters to use and update.
176
+ """
177
+ if not all([connection_params.subscription_id, connection_params.resource_group, connection_params.workspace_name]):
178
+ raise ValueError("Missing required connection parameters to load workspace details from ARM.")
179
+
180
+ api_version = connection_params.api_version or ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION
181
+
182
+ url = (
183
+ f"/subscriptions/{connection_params.subscription_id}"
184
+ f"/resourceGroups/{connection_params.resource_group}"
185
+ f"/providers/Microsoft.Quantum/workspaces/{connection_params.workspace_name}"
186
+ )
187
+
188
+ request = HttpRequest(
189
+ method="GET",
190
+ url=self._client.format_url(url),
191
+ params={"api-version": api_version},
192
+ )
193
+
194
+ try:
195
+ response = self._client.send_request(request)
196
+ response.raise_for_status()
197
+ workspace_data = response.json()
198
+ except HttpResponseError as e:
199
+ if e.status_code == HTTPStatus.NOT_FOUND:
200
+ raise ValueError(
201
+ f"Azure Quantum workspace '{connection_params.workspace_name}' "
202
+ f"not found in resource group '{connection_params.resource_group}' "
203
+ f"and subscription '{connection_params.subscription_id}'. "
204
+ f"{self.CONNECT_DOC_MESSAGE}"
205
+ ) from e
206
+ # Re-raise for other HTTP errors
207
+ raise
208
+ except Exception as e:
209
+ raise RuntimeError(
210
+ f"Could not load workspace details from ARM: {str(e)}.\n{self.CONNECT_DOC_MESSAGE}"
211
+ ) from e
212
+
213
+ # Extract and apply location
214
+ location = workspace_data.get("location")
215
+ if location:
216
+ connection_params.location = location
217
+ logger.debug(
218
+ "Updated workspace location from ARM: %s",
219
+ location
220
+ )
221
+ else:
222
+ raise ValueError(
223
+ f"Failed to retrieve location for workspace '{connection_params.workspace_name}'. "
224
+ f"Please check that workspace is in valid state."
225
+ )
226
+
227
+ # Extract and apply endpoint URI from properties
228
+ properties = workspace_data.get("properties", {})
229
+ endpoint_uri = properties.get("endpointUri")
230
+ if endpoint_uri:
231
+ connection_params.quantum_endpoint = endpoint_uri
232
+ logger.debug(
233
+ "Updated workspace endpoint from ARM: %s", connection_params.quantum_endpoint
234
+ )
235
+ else:
236
+ raise ValueError(
237
+ f"Failed to retrieve endpoint uri for workspace '{connection_params.workspace_name}'. "
238
+ f"Please check that workspace is in valid state."
239
+ )
@@ -20,6 +20,8 @@ from azure.quantum._constants import (
20
20
  EnvironmentVariables,
21
21
  ConnectionConstants,
22
22
  GUID_REGEX_PATTERN,
23
+ VALID_WORKSPACE_NAME_PATTERN,
24
+ VALID_AZURE_REGIONS,
23
25
  )
24
26
 
25
27
  class WorkspaceConnectionParams:
@@ -46,9 +48,19 @@ class WorkspaceConnectionParams:
46
48
  ResourceGroupName=(?P<resource_group>[^\s;]+);
47
49
  WorkspaceName=(?P<workspace_name>[^\s;]+);
48
50
  ApiKey=(?P<api_key>[^\s;]+);
49
- QuantumEndpoint=(?P<quantum_endpoint>https://(?P<location>[^\s\.]+).quantum(?:-test)?.azure.com/);
51
+ QuantumEndpoint=(?P<quantum_endpoint>https://(?P<location>[a-zA-Z0-9]+)(?:-v2)?.quantum(?:-test)?.azure.com/);
50
52
  """,
51
53
  re.VERBOSE | re.IGNORECASE)
54
+
55
+ WORKSPACE_NOT_FULLY_SPECIFIED_MSG = """
56
+ Azure Quantum workspace not fully specified.
57
+ Please specify one of the following:
58
+ 1) A valid resource ID.
59
+ 2) A valid combination of subscription ID,
60
+ resource group name, and workspace name.
61
+ 3) A valid connection string (via Workspace.from_connection_string()).
62
+ 4) A valid workspace name.
63
+ """
52
64
 
53
65
  def __init__(
54
66
  self,
@@ -85,6 +97,8 @@ class WorkspaceConnectionParams:
85
97
  self.client_id = None
86
98
  self.tenant_id = None
87
99
  self.api_version = None
100
+ # Track if connection string was used
101
+ self._used_connection_string = False
88
102
  # callback to create a new client if needed
89
103
  # for example, when changing the user agent
90
104
  self.on_new_client_request = on_new_client_request
@@ -108,6 +122,81 @@ class WorkspaceConnectionParams:
108
122
  workspace_name=workspace_name,
109
123
  )
110
124
  self.apply_resource_id(resource_id=resource_id)
125
+ # Validate connection parameters if they are set
126
+ self._validate_connection_params()
127
+
128
+ def _validate_connection_params(self):
129
+ self._validate_subscription_id()
130
+ self._validate_resource_group()
131
+ self._validate_workspace_name()
132
+ self._validate_location()
133
+
134
+ def _validate_subscription_id(self):
135
+ # Validate that subscription id is a valid GUID
136
+ if self.subscription_id is not None:
137
+ if not isinstance(self.subscription_id, str):
138
+ raise ValueError("Subscription ID must be a string.")
139
+ if not re.match(f"^{GUID_REGEX_PATTERN}$", self.subscription_id, re.IGNORECASE):
140
+ raise ValueError("Subscription ID must be a valid GUID.")
141
+
142
+ def _validate_resource_group(self):
143
+ # Validate resource group, see https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftresources
144
+ # Length 1-90, valid characters: alphanumeric, underscore, parentheses, hyphen, period (except at end), and Unicode characters:
145
+ # Uppercase Letter - Signified by the Unicode designation "Lu" (letter, uppercase);
146
+ # Lowercase Letter - Signified by the Unicode designation "Ll" (letter, lowercase);
147
+ # Titlecase Letter - Signified by the Unicode designation "Lt" (letter, titlecase);
148
+ # Modifier Letter - Signified by the Unicode designation "Lm" (letter, modifier);
149
+ # Other Letter - Signified by the Unicode designation "Lo" (letter, other);
150
+ # Decimal Digit Number - Signified by the Unicode designation "Nd" (number, decimal digit).
151
+ if self.resource_group is not None:
152
+ if not isinstance(self.resource_group, str):
153
+ raise ValueError("Resource group name must be a string.")
154
+
155
+ if len(self.resource_group) < 1 or len(self.resource_group) > 90:
156
+ raise ValueError(
157
+ "Resource group name must be between 1 and 90 characters long."
158
+ )
159
+
160
+ err_msg = "Resource group name can only include alphanumeric, underscore, parentheses, hyphen, period (except at end), and Unicode characters that match the allowed characters."
161
+ if self.resource_group.endswith('.'):
162
+ raise ValueError(err_msg)
163
+
164
+ import unicodedata
165
+ for i, char in enumerate(self.resource_group):
166
+ category = unicodedata.category(char)
167
+ if not (
168
+ char in ('_', '(', ')', '-', '.') or
169
+ category in ('Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nd')
170
+ ):
171
+ raise ValueError(err_msg)
172
+
173
+ def _validate_workspace_name(self):
174
+ # Validate workspace name, see https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftquantum
175
+ # Length 2-54, valid characters: alphanumerics (a-zA-Z0-9) and hyphens, can't start or end with hyphen
176
+ if self.workspace_name is not None:
177
+ if not isinstance(self.workspace_name, str):
178
+ raise ValueError("Workspace name must be a string.")
179
+
180
+ if len(self.workspace_name) < 2 or len(self.workspace_name) > 54:
181
+ raise ValueError(
182
+ "Workspace name must be between 2 and 54 characters long."
183
+ )
184
+
185
+ err_msg = "Workspace name can only include alphanumerics (a-zA-Z0-9) and hyphens, and cannot start or end with hyphen."
186
+
187
+ if self.workspace_name.startswith('-') or self.workspace_name.endswith('-'):
188
+ raise ValueError(err_msg)
189
+
190
+ if not re.match(VALID_WORKSPACE_NAME_PATTERN, self.workspace_name):
191
+ raise ValueError(err_msg)
192
+
193
+ def _validate_location(self):
194
+ # Validate that location is one of the Azure regions https://learn.microsoft.com/en-us/azure/reliability/regions-list
195
+ if self.location is not None:
196
+ if not isinstance(self.location, str):
197
+ raise ValueError("Location must be a string.")
198
+ if self.location not in VALID_AZURE_REGIONS:
199
+ raise ValueError(f"Location must be one of the Azure regions listed in https://learn.microsoft.com/en-us/azure/reliability/regions-list.")
111
200
 
112
201
  @property
113
202
  def location(self):
@@ -142,19 +231,8 @@ class WorkspaceConnectionParams:
142
231
  def quantum_endpoint(self):
143
232
  """
144
233
  The Azure Quantum data plane endpoint.
145
- Defaults to well-known endpoint based on the environment.
146
- """
147
- if self._quantum_endpoint:
148
- return self._quantum_endpoint
149
- if not self.location:
150
- raise ValueError("Location not specified")
151
- if self.environment is EnvironmentKind.PRODUCTION:
152
- return ConnectionConstants.GET_QUANTUM_PRODUCTION_ENDPOINT(self.location)
153
- if self.environment is EnvironmentKind.CANARY:
154
- return ConnectionConstants.GET_QUANTUM_CANARY_ENDPOINT(self.location)
155
- if self.environment is EnvironmentKind.DOGFOOD:
156
- return ConnectionConstants.GET_QUANTUM_DOGFOOD_ENDPOINT(self.location)
157
- raise ValueError(f"Unknown environment `{self.environment}`.")
234
+ """
235
+ return self._quantum_endpoint
158
236
 
159
237
  @quantum_endpoint.setter
160
238
  def quantum_endpoint(self, value: str):
@@ -235,6 +313,7 @@ class WorkspaceConnectionParams:
235
313
  if not match:
236
314
  raise ValueError("Invalid connection string")
237
315
  self._merge_re_match(match)
316
+ self._used_connection_string = True
238
317
 
239
318
  def merge(
240
319
  self,
@@ -450,6 +529,32 @@ class WorkspaceConnectionParams:
450
529
  full_user_agent = (f"{app_id} {full_user_agent}"
451
530
  if full_user_agent else app_id)
452
531
  return full_user_agent
532
+
533
+ def have_enough_for_discovery(self) -> bool:
534
+ """
535
+ Returns true if we have enough parameters
536
+ to try to find the Azure Quantum Workspace.
537
+ """
538
+ return (self.workspace_name
539
+ and self.get_credential_or_default())
540
+
541
+ def assert_have_enough_for_discovery(self):
542
+ """
543
+ Raises ValueError if we don't have enough parameters
544
+ to try to find the Azure Quantum Workspace.
545
+ """
546
+ if not self.have_enough_for_discovery():
547
+ raise ValueError(self.WORKSPACE_NOT_FULLY_SPECIFIED_MSG)
548
+
549
+ def can_build_resource_id(self) -> bool:
550
+ """
551
+ Returns true if we have all necessary parameters
552
+ to identify the Azure Quantum Workspace resource.
553
+ """
554
+ return (self.subscription_id
555
+ and self.resource_group
556
+ and self.workspace_name
557
+ and self.get_credential_or_default())
453
558
 
454
559
  def is_complete(self) -> bool:
455
560
  """
@@ -460,6 +565,7 @@ class WorkspaceConnectionParams:
460
565
  and self.subscription_id
461
566
  and self.resource_group
462
567
  and self.workspace_name
568
+ and self.quantum_endpoint
463
569
  and self.get_credential_or_default())
464
570
 
465
571
  def assert_complete(self):
@@ -468,15 +574,7 @@ class WorkspaceConnectionParams:
468
574
  to connect to the Azure Quantum Workspace.
469
575
  """
470
576
  if not self.is_complete():
471
- raise ValueError(
472
- """
473
- Azure Quantum workspace not fully specified.
474
- Please specify one of the following:
475
- 1) A valid combination of location and resource ID.
476
- 2) A valid combination of location, subscription ID,
477
- resource group name, and workspace name.
478
- 3) A valid connection string (via Workspace.from_connection_string()).
479
- """)
577
+ raise ValueError(self.WORKSPACE_NOT_FULLY_SPECIFIED_MSG)
480
578
 
481
579
  def default_from_env_vars(self) -> WorkspaceConnectionParams:
482
580
  """
@@ -512,10 +610,13 @@ class WorkspaceConnectionParams:
512
610
  or not self.workspace_name
513
611
  or not self.credential
514
612
  ):
515
- self._merge_connection_params(
516
- connection_params=WorkspaceConnectionParams(
517
- connection_string=os.environ.get(EnvironmentVariables.CONNECTION_STRING)),
518
- merge_default_mode=True)
613
+ env_connection_string = os.environ.get(EnvironmentVariables.CONNECTION_STRING)
614
+ if env_connection_string:
615
+ self._merge_connection_params(
616
+ connection_params=WorkspaceConnectionParams(
617
+ connection_string=env_connection_string),
618
+ merge_default_mode=True)
619
+ self._used_connection_string = True
519
620
  return self
520
621
 
521
622
  @classmethod
azure/quantum/version.py CHANGED
@@ -5,4 +5,4 @@
5
5
  # Copyright (c) Microsoft Corporation. All rights reserved.
6
6
  # Licensed under the MIT License.
7
7
  ##
8
- __version__ = "3.5.0.dev0"
8
+ __version__ = "3.5.1.dev1"
@@ -21,6 +21,7 @@ from typing import (
21
21
  Tuple,
22
22
  Union,
23
23
  )
24
+ from typing_extensions import Self
24
25
  from azure.core.paging import ItemPaged
25
26
  from azure.quantum._client import ServicesClient
26
27
  from azure.quantum._client.models import JobDetails, ItemDetails, SessionDetails
@@ -49,6 +50,7 @@ from azure.quantum.storage import (
49
50
  get_container_uri,
50
51
  ContainerClient
51
52
  )
53
+ from azure.quantum._mgmt_client import WorkspaceMgmtClient
52
54
  if TYPE_CHECKING:
53
55
  from azure.quantum.target import Target
54
56
 
@@ -62,10 +64,11 @@ class Workspace:
62
64
  """
63
65
  Represents an Azure Quantum workspace.
64
66
 
65
- When creating a Workspace object, callers have two options for
67
+ When creating a Workspace object, callers have several options for
66
68
  identifying the Azure Quantum workspace (in order of precedence):
67
- 1. specify a valid location and resource ID; or
68
- 2. specify a valid location, subscription ID, resource group, and workspace name.
69
+ 1. specify a valid resource ID; or
70
+ 2. specify a valid subscription ID, resource group, and workspace name; or
71
+ 3. specify a valid workspace name.
69
72
 
70
73
  You can also use a connection string to specify the connection parameters
71
74
  to an Azure Quantum Workspace by calling
@@ -110,6 +113,12 @@ class Workspace:
110
113
  Add the specified value as a prefix to the HTTP User-Agent header
111
114
  when communicating to the Azure Quantum service.
112
115
  """
116
+
117
+ # Internal parameter names
118
+ _FROM_CONNECTION_STRING_PARAM = '_from_connection_string'
119
+ _QUANTUM_ENDPOINT_PARAM = '_quantum_endpoint'
120
+ _MGMT_CLIENT_PARAM = '_mgmt_client'
121
+
113
122
  def __init__(
114
123
  self,
115
124
  subscription_id: Optional[str] = None,
@@ -122,6 +131,14 @@ class Workspace:
122
131
  user_agent: Optional[str] = None,
123
132
  **kwargs: Any,
124
133
  ) -> None:
134
+ # Extract internal params before passing kwargs to WorkspaceConnectionParams
135
+ # Param to track whether the workspace was created from a connection string
136
+ from_connection_string = kwargs.pop(Workspace._FROM_CONNECTION_STRING_PARAM, False)
137
+ # In case from connection string, quantum_endpoint must be passed
138
+ quantum_endpoint = kwargs.pop(Workspace._QUANTUM_ENDPOINT_PARAM, None)
139
+ # Params to pass a mock in tests
140
+ self._mgmt_client = kwargs.pop(Workspace._MGMT_CLIENT_PARAM, None)
141
+
125
142
  connection_params = WorkspaceConnectionParams(
126
143
  location=location,
127
144
  subscription_id=subscription_id,
@@ -129,21 +146,45 @@ class Workspace:
129
146
  workspace_name=name,
130
147
  credential=credential,
131
148
  resource_id=resource_id,
149
+ quantum_endpoint=quantum_endpoint,
132
150
  user_agent=user_agent,
133
151
  **kwargs
134
152
  ).default_from_env_vars()
135
153
 
136
154
  logger.info("Using %s environment.", connection_params.environment)
137
155
 
138
- connection_params.assert_complete()
156
+ connection_params.assert_have_enough_for_discovery()
139
157
 
140
158
  connection_params.on_new_client_request = self._on_new_client_request
141
159
 
142
160
  self._connection_params = connection_params
143
161
  self._storage = storage
144
- self._subscription_id = connection_params.subscription_id
145
- self._resource_group = connection_params.resource_group
146
- self._workspace_name = connection_params.workspace_name
162
+
163
+ if not self._mgmt_client:
164
+ credential = connection_params.get_credential_or_default()
165
+ self._mgmt_client = WorkspaceMgmtClient(
166
+ credential=credential,
167
+ base_url=connection_params.arm_endpoint,
168
+ user_agent=connection_params.get_full_user_agent(),
169
+ )
170
+
171
+ # pylint: disable=protected-access
172
+ using_connection_string = (
173
+ from_connection_string
174
+ or connection_params._used_connection_string
175
+ )
176
+
177
+ # Populate workspace details from ARG if not using connection string and
178
+ # name is provided but missing subscription and/or resource group
179
+ if not using_connection_string \
180
+ and not connection_params.can_build_resource_id():
181
+ self._mgmt_client.load_workspace_from_arg(connection_params)
182
+
183
+ # Populate workspace details from ARM if not using connection string and not loaded from ARG
184
+ if not using_connection_string and not connection_params.is_complete():
185
+ self._mgmt_client.load_workspace_from_arm(connection_params)
186
+
187
+ connection_params.assert_complete()
147
188
 
148
189
  # Create QuantumClient
149
190
  self._client = self._create_client()
@@ -277,6 +318,8 @@ class Workspace:
277
318
  :rtype: Workspace
278
319
  """
279
320
  connection_params = WorkspaceConnectionParams(connection_string=connection_string)
321
+ kwargs[cls._FROM_CONNECTION_STRING_PARAM] = True
322
+ kwargs[cls._QUANTUM_ENDPOINT_PARAM] = connection_params.quantum_endpoint
280
323
  return cls(
281
324
  subscription_id=connection_params.subscription_id,
282
325
  resource_group=connection_params.resource_group,
@@ -358,9 +401,9 @@ class Workspace:
358
401
  container_name=container_name, blob_name=blob_name
359
402
  )
360
403
  container_uri = client.get_sas_uri(
361
- self._subscription_id,
362
- self._resource_group,
363
- self._workspace_name,
404
+ self.subscription_id,
405
+ self.resource_group,
406
+ self.name,
364
407
  blob_details=blob_details)
365
408
 
366
409
  logger.debug("Container URI from service: %s", container_uri)
@@ -378,9 +421,9 @@ class Workspace:
378
421
  """
379
422
  client = self._get_jobs_client()
380
423
  details = client.create_or_replace(
381
- self._subscription_id,
382
- self._resource_group,
383
- self._workspace_name,
424
+ self.subscription_id,
425
+ self.resource_group,
426
+ self.name,
384
427
  job.details.id,
385
428
  job.details
386
429
  )
@@ -399,14 +442,14 @@ class Workspace:
399
442
  """
400
443
  client = self._get_jobs_client()
401
444
  client.delete(
402
- self._subscription_id,
403
- self._resource_group,
404
- self._workspace_name,
445
+ self.subscription_id,
446
+ self.resource_group,
447
+ self.name,
405
448
  job.details.id)
406
449
  details = client.get(
407
- self._subscription_id,
408
- self._resource_group,
409
- self._workspace_name,
450
+ self.subscription_id,
451
+ self.resource_group,
452
+ self.name,
410
453
  job.id)
411
454
  return Job(self, details)
412
455
 
@@ -426,9 +469,9 @@ class Workspace:
426
469
 
427
470
  client = self._get_jobs_client()
428
471
  details = client.get(
429
- self._subscription_id,
430
- self._resource_group,
431
- self._workspace_name,
472
+ self.subscription_id,
473
+ self.resource_group,
474
+ self.name,
432
475
  job_id)
433
476
  target_factory = TargetFactory(base_cls=Target, workspace=self)
434
477
  # pylint: disable=protected-access
@@ -511,7 +554,7 @@ class Workspace:
511
554
  )
512
555
  orderby = self._create_orderby(orderby_property, is_asc)
513
556
 
514
- return client.list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self._workspace_name, filter=job_filter, orderby=orderby, top = top, skip = skip)
557
+ return client.list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self.name, filter=job_filter, orderby=orderby, top = top, skip = skip)
515
558
 
516
559
  def _get_target_status(
517
560
  self,
@@ -534,9 +577,9 @@ class Workspace:
534
577
  return [
535
578
  (provider.id, target)
536
579
  for provider in self._client.providers.list(
537
- self._subscription_id,
538
- self._resource_group,
539
- self._workspace_name)
580
+ self.subscription_id,
581
+ self.resource_group,
582
+ self.name)
540
583
  for target in provider.targets
541
584
  if (provider_id is None or provider.id.lower() == provider_id.lower())
542
585
  and (name is None or target.id.lower() == name.lower())
@@ -593,9 +636,9 @@ class Workspace:
593
636
  """
594
637
  client = self._get_quotas_client()
595
638
  return [q.as_dict() for q in client.list(
596
- self._subscription_id,
597
- self._resource_group,
598
- self._workspace_name
639
+ self.subscription_id,
640
+ self.resource_group,
641
+ self.name
599
642
  )]
600
643
 
601
644
  def list_top_level_items(
@@ -666,7 +709,7 @@ class Workspace:
666
709
  )
667
710
  orderby = self._create_orderby(orderby_property, is_asc)
668
711
 
669
- return client.list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self._workspace_name, filter=top_level_item_filter, orderby=orderby, top = top, skip = skip)
712
+ return client.list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self.name, filter=top_level_item_filter, orderby=orderby, top = top, skip = skip)
670
713
 
671
714
  def list_sessions(
672
715
  self,
@@ -727,7 +770,7 @@ class Workspace:
727
770
 
728
771
  orderby = self._create_orderby(orderby_property=orderby_property, is_asc=is_asc)
729
772
 
730
- return client.list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self._workspace_name, filter = session_filter, orderby=orderby, skip=skip, top=top)
773
+ return client.list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self.name, filter = session_filter, orderby=orderby, skip=skip, top=top)
731
774
 
732
775
  def open_session(
733
776
  self,
@@ -744,9 +787,9 @@ class Workspace:
744
787
  """
745
788
  client = self._get_sessions_client()
746
789
  session.details = client.create_or_replace(
747
- self._subscription_id,
748
- self._resource_group,
749
- self._workspace_name,
790
+ self.subscription_id,
791
+ self.resource_group,
792
+ self.name,
750
793
  session.id,
751
794
  session.details)
752
795
 
@@ -765,15 +808,15 @@ class Workspace:
765
808
  client = self._get_sessions_client()
766
809
  if not session.is_in_terminal_state():
767
810
  session.details = client.close(
768
- self._subscription_id,
769
- self._resource_group,
770
- self._workspace_name,
811
+ self.subscription_id,
812
+ self.resource_group,
813
+ self.name,
771
814
  session_id=session.id)
772
815
  else:
773
816
  session.details = client.get(
774
- self._subscription_id,
775
- self._resource_group,
776
- self._workspace_name,
817
+ self.subscription_id,
818
+ self.resource_group,
819
+ self.name,
777
820
  session_id=session.id)
778
821
 
779
822
  if session.target:
@@ -809,9 +852,9 @@ class Workspace:
809
852
  """
810
853
  client = self._get_sessions_client()
811
854
  session_details = client.get(
812
- self._subscription_id,
813
- self._resource_group,
814
- self._workspace_name,
855
+ self.subscription_id,
856
+ self.resource_group,
857
+ self.name,
815
858
  session_id=session_id)
816
859
  result = Session(workspace=self, details=session_details)
817
860
  return result
@@ -873,7 +916,7 @@ class Workspace:
873
916
 
874
917
  orderby = self._create_orderby(orderby_property=orderby_property, is_asc=is_asc)
875
918
 
876
- return client.jobs_list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self._workspace_name, session_id=session_id, filter = session_job_filter, orderby=orderby, skip=skip, top=top)
919
+ return client.jobs_list(subscription_id=self.subscription_id, resource_group_name=self.resource_group, workspace_name=self.name, session_id=session_id, filter = session_job_filter, orderby=orderby, skip=skip, top=top)
877
920
 
878
921
  def get_container_uri(
879
922
  self,
@@ -1023,4 +1066,16 @@ class Workspace:
1023
1066
  return orderby
1024
1067
  else:
1025
1068
  return None
1026
-
1069
+
1070
+ def close(self) -> None:
1071
+ self._mgmt_client.close()
1072
+ self._client.close()
1073
+
1074
+ def __enter__(self) -> Self:
1075
+ self._client.__enter__()
1076
+ self._mgmt_client.__enter__()
1077
+ return self
1078
+
1079
+ def __exit__(self, *exc_details: Any) -> None:
1080
+ self._mgmt_client.__exit__(*exc_details)
1081
+ self._client.__exit__(*exc_details)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: azure-quantum
3
- Version: 3.5.0.dev0
3
+ Version: 3.5.1.dev1
4
4
  Summary: Python client for Azure Quantum
5
5
  Home-page: https://github.com/microsoft/azure-quantum-python
6
6
  Author: Microsoft
@@ -79,15 +79,14 @@ To get started, visit the following Quickstart guides:
79
79
 
80
80
  ## General usage ##
81
81
 
82
- To connect to your Azure Quantum Workspace, go to the [Azure Portal](https://portal.azure.com), navigate to your Workspace and copy-paste the resource ID and location into the code snippet below.
82
+ To connect to your Azure Quantum Workspace, go to the [Azure Portal](https://portal.azure.com), navigate to your Workspace and copy-paste the resource ID into the code snippet below.
83
83
 
84
84
  ```python
85
85
  from azure.quantum import Workspace
86
86
 
87
- # Enter your Workspace details (resource ID and location) below
87
+ # Enter your Workspace resource ID below
88
88
  workspace = Workspace(
89
- resource_id="",
90
- location=""
89
+ resource_id=""
91
90
  )
92
91
  ```
93
92
 
@@ -1,16 +1,17 @@
1
1
  azure/quantum/__init__.py,sha256=Za8xZY4lzFkW8m4ero-bqrfN437D2NRukM77ukb4GPM,508
2
- azure/quantum/_constants.py,sha256=nDL_QrGdI_Zz_cvTB9nVgfE7J6A_Boo1ollMYqsiEBs,3499
3
- azure/quantum/_workspace_connection_params.py,sha256=70T6JHa72tPLM5i7IEIBMbqa3gxXXtDmu_uORWloo50,21553
2
+ azure/quantum/_constants.py,sha256=EVGCZh2TJPkTOvti5Wq2V9mRFf4_gueiRZrXtCcPO9I,5110
3
+ azure/quantum/_mgmt_client.py,sha256=9aCLHsrjLMnWKWkWOpRkW6fMQeSRbIy13q4Rjx4z_W8,10253
4
+ azure/quantum/_workspace_connection_params.py,sha256=lVJpEdX6qz4MywnCv2h2ANFQYJJXRDAVNf5MfobLWnM,26636
4
5
  azure/quantum/storage.py,sha256=_4bMniDk9LrB_K5CQwuCivJFZXdmhRvU2b6Z3xxXw9I,12556
5
- azure/quantum/version.py,sha256=0IKkbEdcvscSEt1GMWf4Ok5ZgtBarauA8zjtpkTZpHg,240
6
- azure/quantum/workspace.py,sha256=9oO4vjwIn1pdHFLZCQcEPQ_xjQjTNO4UIWSBQpj6Cgo,35574
6
+ azure/quantum/version.py,sha256=Sz0V0lSXVDypobmOgKo14ja7lYHWL0GCDWQkcxa3ibE,240
7
+ azure/quantum/workspace.py,sha256=dc2xWEismuh4MI0lDqhsWKBj8ik4gTk98nANVKoWbcQ,37833
7
8
  azure/quantum/_client/__init__.py,sha256=P3K9ffYchHhHjJhCEAEwutSD3xRW92XDUQNYDjwhYqI,1056
8
9
  azure/quantum/_client/_client.py,sha256=7MRvLQZYcg0PKf906ulEOtF6H22Km1aa0FFj3O6LxXc,6734
9
10
  azure/quantum/_client/_configuration.py,sha256=FFUHJPp6X0015GpD-YLmYCoq8GI86nHCnDS_THTSHbg,4184
10
11
  azure/quantum/_client/_model_base.py,sha256=hu7OdRS2Ra1igfBo-R3zS3dzO3YhFC4FGHJ_WiyZgNg,43736
11
12
  azure/quantum/_client/_patch.py,sha256=YTV6yZ9bRfBBaw2z7v4MdzR-zeHkdtKkGb4SU8C25mE,694
12
13
  azure/quantum/_client/_serialization.py,sha256=bBl0y0mh-0sDd-Z8_dj921jQQhqhYWTOl59qSLuk01M,86686
13
- azure/quantum/_client/_version.py,sha256=ERfqFUpSSHTzPMC0LLLdaaXm30TV2v719rPGNSuyYXo,500
14
+ azure/quantum/_client/_version.py,sha256=aO-J45LO8r4dDvSR1zoTgMYrlW9lfwE_c0B_5v5XJJY,500
14
15
  azure/quantum/_client/py.typed,sha256=dcrsqJrcYfTX-ckLFJMTaj6mD8aDe2u0tkQG-ZYxnEg,26
15
16
  azure/quantum/_client/models/__init__.py,sha256=MR2av7s_tCP66hicN9JXCmTngJ4_-ozM4cmblGjPwn8,1971
16
17
  azure/quantum/_client/models/_enums.py,sha256=oSJm7cDvyRwYQLZ50XLNWEfHJJ5c_XG8MlOux2PFT7Q,4706
@@ -58,7 +59,7 @@ azure/quantum/target/pasqal/target.py,sha256=K_vqavov6gvS84voRKeBx9pO8g4LrtWrlZ5
58
59
  azure/quantum/target/rigetti/__init__.py,sha256=I1vyzZBYGI540pauTqJd0RSSyTShGqkEL7Yjo25_RNY,378
59
60
  azure/quantum/target/rigetti/result.py,sha256=Xdb5LSwOkJssluDVDhg2gxHLgdvXpwCmA9QMr3jx3VA,2372
60
61
  azure/quantum/target/rigetti/target.py,sha256=HFrng9SigibPn_2KWhD5qWHiP_RNz3CNRkv288tf-z8,7586
61
- azure_quantum-3.5.0.dev0.dist-info/METADATA,sha256=bAE_JpdePnEXVdj0s9lR9ELahtuMC5C4Y7vZqtQnkwA,6759
62
- azure_quantum-3.5.0.dev0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
63
- azure_quantum-3.5.0.dev0.dist-info/top_level.txt,sha256=S7DhWV9m80TBzAhOFjxDUiNbKszzoThbnrSz5MpbHSQ,6
64
- azure_quantum-3.5.0.dev0.dist-info/RECORD,,
62
+ azure_quantum-3.5.1.dev1.dist-info/METADATA,sha256=i3nrDPBrZdu7BK-3DrTL4rFioTxc381x1_x6VluZEkI,6705
63
+ azure_quantum-3.5.1.dev1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
64
+ azure_quantum-3.5.1.dev1.dist-info/top_level.txt,sha256=S7DhWV9m80TBzAhOFjxDUiNbKszzoThbnrSz5MpbHSQ,6
65
+ azure_quantum-3.5.1.dev1.dist-info/RECORD,,