From 96337352be437888cd20debef14b64ba783f1de5 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 17 Jun 2020 14:42:31 -0700 Subject: [PATCH 01/19] Support resource ID in %azure.connect --- src/AzureClient/AzureClient.cs | 25 ++++++-- src/AzureClient/AzureClient.csproj | 2 +- src/AzureClient/Magic/ConnectMagic.cs | 88 +++++++++++++++++++-------- src/Core/Core.csproj | 6 +- src/Jupyter/Magic/AbstractMagic.cs | 5 +- src/Tests/AzureClientMagicTests.cs | 20 +++++- src/Tool/appsettings.json | 28 ++++----- 7 files changed, 118 insertions(+), 56 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c03ad4a61f..5202e22698 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -66,21 +66,34 @@ public async Task ConnectAsync(IChannel channel, string storageAccountConnectionString, bool refreshCredentials = false) { - ConnectionString = storageAccountConnectionString; - var azureEnvironment = AzureEnvironment.Create(subscriptionId); - ActiveWorkspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials); - if (ActiveWorkspace == null) + IAzureWorkspace? workspace = null; + try + { + workspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials); + } + catch (Exception e) + { + channel.Stderr($"The connection to the Azure Quantum workspace could not be completed. Please check the provided parameters and try again."); + channel.Stderr($"Error details: {e.Message}"); + return AzureClientError.WorkspaceNotFound.ToExecutionResult(); + } + + if (workspace == null) { return AzureClientError.AuthenticationFailed.ToExecutionResult(); } - AvailableProviders = await ActiveWorkspace.GetProvidersAsync(); - if (AvailableProviders == null) + var providers = await workspace.GetProvidersAsync(); + if (providers == null) { return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } + ActiveWorkspace = workspace; + AvailableProviders = providers; + ConnectionString = storageAccountConnectionString; + channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); return ValidExecutionTargets.ToExecutionResult(); diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index 995ae372bb..8d7b029f32 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 559c04e3e7..27ddfc2782 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -35,54 +36,62 @@ public ConnectMagic(IAzureClient azureClient) "azure.connect", new Documentation { - Summary = "Connects to an Azure workspace or displays current connection status.", + Summary = "Connects to an Azure Quantum workspace or displays current connection status.", Description = @" This magic command allows for connecting to an Azure Quantum workspace - as specified by a valid subscription ID, resource group name, workspace name, - and storage account connection string. + as specified by the resource ID of the workspace or by a combination of + subscription ID, resource group name, and workspace name. - If the connection is successful, a list of the available execution targets + If the connection is successful, a list of the available Q# execution targets in the Azure Quantum workspace will be displayed. ".Dedent(), Examples = new[] { @" - Print information about the current connection: + Connect to an Azure Quantum workspace using its resource ID: ``` - In []: %azure.connect + In []: %azure.connect ""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. - + ``` ".Dedent(), $@" - Connect to an Azure Quantum workspace: + Connect to an Azure Quantum workspace using individual parameters: ``` - In []: %azure.connect {ParameterNameSubscriptionId}=SUBSCRIPTION_ID - {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME - {ParameterNameWorkspaceName}=WORKSPACE_NAME - {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING + In []: %azure.connect {ParameterNameSubscriptionId}=""SUBSCRIPTION_ID"" + {ParameterNameResourceGroupName}=""RESOURCE_GROUP_NAME"" + {ParameterNameWorkspaceName}=""WORKSPACE_NAME"" + {ParameterNameStorageAccountConnectionString}=""CONNECTION_STRING"" Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. - + ``` + The `{ParameterNameStorageAccountConnectionString}` parameter is necessary only if the + specified Azure Quantum workspace was not linked to a storage account at creation time. ".Dedent(), $@" - Connect to an Azure Quantum workspace and force a credential prompt: + Connect to an Azure Quantum workspace and force a credential prompt using + the `{ParameterNameRefresh}` option: ``` - In []: %azure.connect {ParameterNameRefresh} - {ParameterNameSubscriptionId}=SUBSCRIPTION_ID - {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME - {ParameterNameWorkspaceName}=WORKSPACE_NAME - {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING + In []: %azure.connect {ParameterNameRefresh} ""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" Out[]: To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code [login code] to authenticate. Connected to Azure Quantum workspace WORKSPACE_NAME. - + ``` - Use the `{ParameterNameRefresh}` option if you want to bypass any saved or cached + The `{ParameterNameRefresh}` option bypasses any saved or cached credentials when connecting to Azure. ".Dedent(), + + @" + Print information about the current connection: + ``` + In []: %azure.connect + Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. + + ``` + ".Dedent(), }, }) {} @@ -93,16 +102,41 @@ credentials when connecting to Azure. public override async Task RunAsync(string input, IChannel channel) { var inputParameters = ParseInputParameters(input); - - var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString); - if (string.IsNullOrEmpty(storageAccountConnectionString)) + if (!inputParameters.Any()) { return await AzureClient.GetConnectionStatusAsync(channel); } - var subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId); - var resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName); - var workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName); + var subscriptionId = string.Empty; + var resourceGroupName = string.Empty; + var workspaceName = string.Empty; + + // A valid resource ID looks like: + // /subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME + var resourceIdRegex = new Regex( + @"^\/subscriptions\/([a-zA-Z0-9\-]*)\/resourceGroups\/([^\s\/]*)\/providers\/Microsoft\.Quantum\/Workspaces\/([^\s\/]*)$"); + var resourceIdParameters = inputParameters.Where((pair) => resourceIdRegex.IsMatch(pair.Key)); + if (resourceIdParameters.Any()) + { + // match.Groups will be a GroupCollection containing four Group objects: + // -> match.Groups[0]: The full resource ID for the Azure Quantum workspace + // -> match.Groups[1]: The Azure subscription ID + // -> match.Groups[2]: The Azure resource group name + // -> match.Groups[3]: The Azure Quantum workspace name + var match = resourceIdRegex.Match(resourceIdParameters.First().Key); + subscriptionId = match.Groups[1].Value; + resourceGroupName = match.Groups[2].Value; + workspaceName = match.Groups[3].Value; + } + else + { + // look for each of the parameters individually + subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId, defaultValue: string.Empty); + resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName, defaultValue: string.Empty); + workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName, defaultValue: string.Empty); + } + + var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString, defaultValue: string.Empty); var refreshCredentials = inputParameters.DecodeParameter(ParameterNameRefresh, defaultValue: false); return await AzureClient.ConnectAsync( channel, diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 11460fc08e..872d3a96af 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 64a1326266..ea90742398 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -122,14 +122,15 @@ public static Dictionary ParseInputParameters(string input, stri foreach (string arg in args) { var tokens = arg.Split("=", 2); - var key = tokens[0].Trim(); + var regexBeginEndQuotes = @"^['""]|['""]$"; + var key = Regex.Replace(tokens[0].Trim(), regexBeginEndQuotes, string.Empty); var value = tokens.Length switch { // If there was no value provided explicitly, treat it as an implicit "true" value 1 => true as object, // Trim whitespace and also enclosing single-quotes or double-quotes before returning - 2 => Regex.Replace(tokens[1].Trim(), @"^['""]|['""]$", string.Empty) as object, + 2 => Regex.Replace(tokens[1].Trim(), regexBeginEndQuotes, string.Empty) as object, // We called arg.Split("=", 2), so there should never be more than 2 _ => throw new InvalidOperationException() diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 51763fdbac..4cec65958e 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -11,6 +11,7 @@ using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; using Microsoft.Extensions.DependencyInjection; +using System; namespace Tests.IQSharp { @@ -26,7 +27,7 @@ public static void Test(this MagicSymbol magic, string input, ExecuteStatus expe [TestClass] public class AzureClientMagicTests { - private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; + private readonly string subscriptionId = Guid.NewGuid().ToString(); private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; private readonly string workspaceName = "TEST_WORKSPACE_NAME"; private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; @@ -40,11 +41,24 @@ public void TestConnectMagic() var azureClient = new MockAzureClient(); var connectMagic = new ConnectMagic(azureClient); + // no input + connectMagic.Test(string.Empty); + Assert.AreEqual(AzureClientAction.GetConnectionStatus, azureClient.LastAction); + // unrecognized input connectMagic.Test($"invalid"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetConnectionStatus); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + + // valid input with resource ID + connectMagic.Test($"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}"); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + Assert.IsFalse(azureClient.RefreshCredentials); + Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); + Assert.AreEqual(resourceGroupName, azureClient.ResourceGroupName); + Assert.AreEqual(workspaceName, azureClient.WorkspaceName); + Assert.AreEqual(string.Empty, azureClient.ConnectionString); - // valid input + // valid input with individual parameters connectMagic.Test( @$"subscriptionId={subscriptionId} resourceGroupName={resourceGroupName} diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index f6975e9c6c..9e11b240e2 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,24 +6,24 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2006.403", + "Microsoft.Quantum.Compiler::0.11.2006.1615-beta", - "Microsoft.Quantum.CsharpGeneration::0.11.2006.403", - "Microsoft.Quantum.Development.Kit::0.11.2006.403", - "Microsoft.Quantum.Simulators::0.11.2006.403", - "Microsoft.Quantum.Xunit::0.11.2006.403", + "Microsoft.Quantum.CsharpGeneration::0.11.2006.1615-beta", + "Microsoft.Quantum.Development.Kit::0.11.2006.1615-beta", + "Microsoft.Quantum.Simulators::0.11.2006.1615-beta", + "Microsoft.Quantum.Xunit::0.11.2006.1615-beta", - "Microsoft.Quantum.Standard::0.11.2006.403", - "Microsoft.Quantum.Chemistry::0.11.2006.403", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.403", - "Microsoft.Quantum.Numerics::0.11.2006.403", + "Microsoft.Quantum.Standard::0.11.2006.1615-beta", + "Microsoft.Quantum.Chemistry::0.11.2006.1615-beta", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.1615-beta", + "Microsoft.Quantum.Numerics::0.11.2006.1615-beta", - "Microsoft.Quantum.Katas::0.11.2006.403", + "Microsoft.Quantum.Katas::0.11.2006.1615-beta", - "Microsoft.Quantum.Research::0.11.2006.403", + "Microsoft.Quantum.Research::0.11.2006.1615-beta", - "Microsoft.Quantum.Providers.IonQ::0.11.2006.403", - "Microsoft.Quantum.Providers.Honeywell::0.11.2006.403", - "Microsoft.Quantum.Providers.QCI::0.11.2006.403", + "Microsoft.Quantum.Providers.IonQ::0.11.2006.1615-beta", + "Microsoft.Quantum.Providers.Honeywell::0.11.2006.1615-beta", + "Microsoft.Quantum.Providers.QCI::0.11.2006.1615-beta", ] } From 29fc3cf1f77f3bb0d86691cfe440f1f423842ffc Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 18 Jun 2020 11:59:13 -0700 Subject: [PATCH 02/19] Update parameter names to match standalone executable --- src/AzureClient/Magic/ConnectMagic.cs | 10 +++++----- src/Tests/AzureClientMagicTests.cs | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 27ddfc2782..b56a28decf 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -19,10 +19,10 @@ namespace Microsoft.Quantum.IQSharp.AzureClient public class ConnectMagic : AzureClientMagicBase { private const string ParameterNameRefresh = "refresh"; - private const string ParameterNameStorageAccountConnectionString = "storageAccountConnectionString"; - private const string ParameterNameSubscriptionId = "subscriptionId"; - private const string ParameterNameResourceGroupName = "resourceGroupName"; - private const string ParameterNameWorkspaceName = "workspaceName"; + private const string ParameterNameStorageAccountConnectionString = "storage"; + private const string ParameterNameSubscriptionId = "subscription"; + private const string ParameterNameResourceGroupName = "resourceGroup"; + private const string ParameterNameWorkspaceName = "workspace"; /// /// Initializes a new instance of the class. @@ -62,7 +62,7 @@ in the Azure Quantum workspace will be displayed. In []: %azure.connect {ParameterNameSubscriptionId}=""SUBSCRIPTION_ID"" {ParameterNameResourceGroupName}=""RESOURCE_GROUP_NAME"" {ParameterNameWorkspaceName}=""WORKSPACE_NAME"" - {ParameterNameStorageAccountConnectionString}=""CONNECTION_STRING"" + {ParameterNameStorageAccountConnectionString}=""STORAGE_ACCOUNT_CONNECTION_STRING"" Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. ``` diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 4cec65958e..198ad53028 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -60,10 +60,10 @@ public void TestConnectMagic() // valid input with individual parameters connectMagic.Test( - @$"subscriptionId={subscriptionId} - resourceGroupName={resourceGroupName} - workspaceName={workspaceName} - storageAccountConnectionString={storageAccountConnectionString}"); + @$"subscription={subscriptionId} + resourceGroup={resourceGroupName} + workspace={workspaceName} + storage={storageAccountConnectionString}"); Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); Assert.IsFalse(azureClient.RefreshCredentials); Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); @@ -73,10 +73,10 @@ public void TestConnectMagic() // valid input with extra whitespace and quotes connectMagic.Test( - @$"subscriptionId = {subscriptionId} - resourceGroupName= ""{resourceGroupName}"" - workspaceName ={workspaceName} - storageAccountConnectionString = '{storageAccountConnectionString}'"); + @$"subscription = {subscriptionId} + resourceGroup= ""{resourceGroupName}"" + workspace ={workspaceName} + storage = '{storageAccountConnectionString}'"); Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); Assert.IsFalse(azureClient.RefreshCredentials); Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); @@ -86,10 +86,10 @@ public void TestConnectMagic() // valid input with forced login connectMagic.Test( - @$"refresh subscriptionId={subscriptionId} - resourceGroupName={resourceGroupName} - workspaceName={workspaceName} - storageAccountConnectionString={storageAccountConnectionString}"); + @$"refresh subscription={subscriptionId} + resourceGroup={resourceGroupName} + workspace={workspaceName} + storage={storageAccountConnectionString}"); Assert.IsTrue(azureClient.RefreshCredentials); } From f71c7ad253dbf4b46278ea010aa9ab95c5cdc4f9 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 07:22:00 -0700 Subject: [PATCH 03/19] Update Python API and tests --- src/Python/qsharp/azure.py | 4 ++-- src/Python/qsharp/tests/test_azure.py | 32 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index f2632e8d32..1b0e0250b3 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -91,8 +91,8 @@ def __eq__(self, other): ## FUNCTIONS ## -def connect(**params) -> List[AzureTarget]: - result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) +def connect(resourceId : str = '', **params) -> List[AzureTarget]: + result = qsharp.client._execute_magic(f"azure.connect {resourceId}", raise_on_stderr=False, **params) if "error_code" in result: raise AzureError(result) return [AzureTarget(target) for target in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index f626a759c6..2b9fed513e 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -37,10 +37,10 @@ def test_empty_workspace(): assert exception_info.value.error_name == "NotConnected" targets = qsharp.azure.connect( - storageAccountConnectionString="test", - subscriptionId="test", - resourceGroupName="test", - workspaceName="test" + storage="test", + subscription="test", + resourceGroup="test", + workspace="test" ) assert targets == [] @@ -51,19 +51,31 @@ def test_empty_workspace(): jobs = qsharp.azure.jobs() assert jobs == [] -def test_workspace_with_providers(): +def test_create_workspace_with_parameters(): """ - Tests behavior of a mock workspace with mock providers. + Tests creation of a mock workspace with mock providers using parameters. """ targets = qsharp.azure.connect( - storageAccountConnectionString="test", - subscriptionId="test", - resourceGroupName="test", - workspaceName="WorkspaceNameWithMockProviders" + storage="test", + subscription="test", + resourceGroup="test", + workspace="WorkspaceNameWithMockProviders" ) assert isinstance(targets, list) assert len(targets) > 0 +def test_workspace_with_providers(): + """ + Tests behavior of a mock workspace created via resource ID with mock providers. + """ + subscriptionId = "test" + resourceGroupName = "test" + workspaceName = "WorkspaceNameWithMockProviders" + targets = qsharp.azure.connect( + f"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}") + assert isinstance(targets, list) + assert len(targets) > 0 + with pytest.raises(AzureError) as exception_info: qsharp.azure.target() assert exception_info.value.error_name == "NoTarget" From be57cc84d1e2c7e1ec5bbdf6ebe2094da910c87b Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 14:50:45 -0700 Subject: [PATCH 04/19] Use named parameter for resource ID --- src/AzureClient/Magic/ConnectMagic.cs | 13 +++++++------ src/Python/qsharp/azure.py | 4 ++-- src/Python/qsharp/tests/test_azure.py | 2 +- src/Tests/AzureClientMagicTests.cs | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 219745ecf9..1890bb2b64 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -23,6 +23,7 @@ public class ConnectMagic : AzureClientMagicBase private const string ParameterNameSubscriptionId = "subscription"; private const string ParameterNameResourceGroupName = "resourceGroup"; private const string ParameterNameWorkspaceName = "workspace"; + private const string ParameterNameResourceId = "resourceId"; /// /// Initializes a new instance of the class. @@ -47,10 +48,10 @@ in the Azure Quantum workspace will be displayed. ".Dedent(), Examples = new[] { - @" + $@" Connect to an Azure Quantum workspace using its resource ID: ``` - In []: %azure.connect ""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" + In []: %azure.connect {ParameterNameResourceId}=""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. ``` @@ -74,7 +75,7 @@ specified Azure Quantum workspace was not linked to a storage account at creatio Connect to an Azure Quantum workspace and force a credential prompt using the `{ParameterNameRefresh}` option: ``` - In []: %azure.connect {ParameterNameRefresh} ""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" + In []: %azure.connect {ParameterNameRefresh} {ParameterNameResourceId}=""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" Out[]: To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code [login code] to authenticate. Connected to Azure Quantum workspace WORKSPACE_NAME. @@ -107,6 +108,7 @@ public override async Task RunAsync(string input, IChannel chan return await AzureClient.GetConnectionStatusAsync(channel); } + var resourceId = inputParameters.DecodeParameter(ParameterNameResourceId, defaultValue: string.Empty); var subscriptionId = string.Empty; var resourceGroupName = string.Empty; var workspaceName = string.Empty; @@ -115,15 +117,14 @@ public override async Task RunAsync(string input, IChannel chan // /subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME var resourceIdRegex = new Regex( @"^\/subscriptions\/([a-zA-Z0-9\-]*)\/resourceGroups\/([^\s\/]*)\/providers\/Microsoft\.Quantum\/Workspaces\/([^\s\/]*)$"); - var resourceIdParameters = inputParameters.Where((pair) => resourceIdRegex.IsMatch(pair.Key)); - if (resourceIdParameters.Any()) + if (!string.IsNullOrEmpty(resourceId) && resourceIdRegex.IsMatch(resourceId)) { // match.Groups will be a GroupCollection containing four Group objects: // -> match.Groups[0]: The full resource ID for the Azure Quantum workspace // -> match.Groups[1]: The Azure subscription ID // -> match.Groups[2]: The Azure resource group name // -> match.Groups[3]: The Azure Quantum workspace name - var match = resourceIdRegex.Match(resourceIdParameters.First().Key); + var match = resourceIdRegex.Match(resourceId); subscriptionId = match.Groups[1].Value; resourceGroupName = match.Groups[2].Value; workspaceName = match.Groups[3].Value; diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index 47e3d15594..afbe53c3fb 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -100,8 +100,8 @@ def __eq__(self, other) -> bool: ## FUNCTIONS ## -def connect(resourceId : str = '', **params) -> List[AzureTarget]: - result = qsharp.client._execute_magic(f"azure.connect {resourceId}", raise_on_stderr=False, **params) +def connect(**params) -> List[AzureTarget]: + result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) if "error_code" in result: raise AzureError(result) return [AzureTarget(target) for target in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index 2b9fed513e..f30f4e329d 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -72,7 +72,7 @@ def test_workspace_with_providers(): resourceGroupName = "test" workspaceName = "WorkspaceNameWithMockProviders" targets = qsharp.azure.connect( - f"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}") + resourceId=f"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}") assert isinstance(targets, list) assert len(targets) > 0 diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 198ad53028..9ac429481d 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -50,7 +50,7 @@ public void TestConnectMagic() Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); // valid input with resource ID - connectMagic.Test($"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}"); + connectMagic.Test($"resourceId=/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}"); Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); Assert.IsFalse(azureClient.RefreshCredentials); Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); From 6f3fb725389fb0e3d2cef1e0f5829fa7d933dd77 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 15:53:11 -0700 Subject: [PATCH 05/19] Avoid calling Regex.IsMatch Co-authored-by: Sarah Marshall <33814365+samarsha@users.noreply.github.com> --- src/AzureClient/Magic/ConnectMagic.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 1890bb2b64..0d80f3f0e3 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -117,7 +117,8 @@ public override async Task RunAsync(string input, IChannel chan // /subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME var resourceIdRegex = new Regex( @"^\/subscriptions\/([a-zA-Z0-9\-]*)\/resourceGroups\/([^\s\/]*)\/providers\/Microsoft\.Quantum\/Workspaces\/([^\s\/]*)$"); - if (!string.IsNullOrEmpty(resourceId) && resourceIdRegex.IsMatch(resourceId)) + var match = resourceIdRegex.Match(resourceId); + if (match.Success) { // match.Groups will be a GroupCollection containing four Group objects: // -> match.Groups[0]: The full resource ID for the Azure Quantum workspace @@ -148,4 +149,4 @@ public override async Task RunAsync(string input, IChannel chan refreshCredentials); } } -} \ No newline at end of file +} From b68675d2b648e75a7a7d522083aae60433fbbc9d Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 16:12:10 -0700 Subject: [PATCH 06/19] Address PR feedback and improve tests --- src/AzureClient/AzureClient.cs | 2 ++ src/AzureClient/Magic/ConnectMagic.cs | 17 ++++++++++---- src/Python/qsharp/tests/test_azure.py | 32 ++++++++++++++++++++++----- src/Tests/AzureClientMagicTests.cs | 11 +++++++++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 3fe8992553..9a1ff040e3 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -99,6 +99,8 @@ public async Task ConnectAsync(IChannel channel, ActiveWorkspace = workspace; AvailableProviders = providers; ConnectionString = storageAccountConnectionString; + ActiveTarget = null; + MostRecentJobId = string.Empty; channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 0d80f3f0e3..7788aadd8f 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -57,6 +57,17 @@ in the Azure Quantum workspace will be displayed. ``` ".Dedent(), + $@" + Connect to an Azure Quantum workspace using its resource ID and a storage account connection string, + which is required for workspaces that do not have a linked storage account: + ``` + In []: %azure.connect {ParameterNameResourceId}=""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" + {ParameterNameStorageAccountConnectionString}=""STORAGE_ACCOUNT_CONNECTION_STRING"" + Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. + + ``` + ".Dedent(), + $@" Connect to an Azure Quantum workspace using individual parameters: ``` @@ -115,9 +126,8 @@ public override async Task RunAsync(string input, IChannel chan // A valid resource ID looks like: // /subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME - var resourceIdRegex = new Regex( - @"^\/subscriptions\/([a-zA-Z0-9\-]*)\/resourceGroups\/([^\s\/]*)\/providers\/Microsoft\.Quantum\/Workspaces\/([^\s\/]*)$"); - var match = resourceIdRegex.Match(resourceId); + var match = Regex.Match(resourceId, + @"^/subscriptions/([a-fA-F0-9-]*)/resourceGroups/([^\s/]*)/providers/Microsoft\.Quantum/Workspaces/([^\s/]*)$"); if (match.Success) { // match.Groups will be a GroupCollection containing four Group objects: @@ -125,7 +135,6 @@ public override async Task RunAsync(string input, IChannel chan // -> match.Groups[1]: The Azure subscription ID // -> match.Groups[2]: The Azure resource group name // -> match.Groups[3]: The Azure Quantum workspace name - var match = resourceIdRegex.Match(resourceId); subscriptionId = match.Groups[1].Value; resourceGroupName = match.Groups[2].Value; workspaceName = match.Groups[3].Value; diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index f30f4e329d..ae350d309f 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -51,9 +51,9 @@ def test_empty_workspace(): jobs = qsharp.azure.jobs() assert jobs == [] -def test_create_workspace_with_parameters(): +def test_workspace_create_with_parameters(): """ - Tests creation of a mock workspace with mock providers using parameters. + Tests behavior of a mock workspace with providers, using parameters to connect. """ targets = qsharp.azure.connect( storage="test", @@ -64,11 +64,13 @@ def test_create_workspace_with_parameters(): assert isinstance(targets, list) assert len(targets) > 0 -def test_workspace_with_providers(): + _test_workspace_with_providers_after_connection() + +def test_workspace_create_with_resource_id(): """ - Tests behavior of a mock workspace created via resource ID with mock providers. + Tests behavior of a mock workspace with providers, using resource ID to connect. """ - subscriptionId = "test" + subscriptionId = "f846b2bd-d0e2-4a1d-8141-4c6944a9d387" resourceGroupName = "test" workspaceName = "WorkspaceNameWithMockProviders" targets = qsharp.azure.connect( @@ -76,10 +78,30 @@ def test_workspace_with_providers(): assert isinstance(targets, list) assert len(targets) > 0 + _test_workspace_with_providers_after_connection() + +def test_workspace_create_with_resource_id_and_storage(): + """ + Tests behavior of a mock workspace with providers, using resource ID and storage connection string to connect. + """ + subscriptionId = "f846b2bd-d0e2-4a1d-8141-4c6944a9d387" + resourceGroupName = "test" + workspaceName = "WorkspaceNameWithMockProviders" + storageAccountConnectionString = "test" + targets = qsharp.azure.connect( + resourceId=f"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}", + storage=storageAccountConnectionString) + assert isinstance(targets, list) + assert len(targets) > 0 + + _test_workspace_with_providers_after_connection() + +def _test_workspace_with_providers_after_connection(): with pytest.raises(AzureError) as exception_info: qsharp.azure.target() assert exception_info.value.error_name == "NoTarget" + targets = qsharp.azure.connect() for target in targets: active_target = qsharp.azure.target(target.id) assert isinstance(active_target, AzureTarget) diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 9ac429481d..a588bac79f 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -58,6 +58,17 @@ public void TestConnectMagic() Assert.AreEqual(workspaceName, azureClient.WorkspaceName); Assert.AreEqual(string.Empty, azureClient.ConnectionString); + // valid input with resource ID and storage account connection string + connectMagic.Test( + @$"resourceId=/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName} + storage={storageAccountConnectionString}"); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + Assert.IsFalse(azureClient.RefreshCredentials); + Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); + Assert.AreEqual(resourceGroupName, azureClient.ResourceGroupName); + Assert.AreEqual(workspaceName, azureClient.WorkspaceName); + Assert.AreEqual(storageAccountConnectionString, azureClient.ConnectionString); + // valid input with individual parameters connectMagic.Test( @$"subscription={subscriptionId} From ec1363933173dfa202c3c3fbf6bd5db1c0ecb8d0 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 16:57:54 -0700 Subject: [PATCH 07/19] Empty commit to re-trigger CI build From 5f48066e5d46111bf3fe1f84b62d2eb3af809742 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 17:35:55 -0700 Subject: [PATCH 08/19] Speed up tests, enable debug logging --- build/test.ps1 | 2 +- src/Python/qsharp/tests/test_azure.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/test.ps1 b/build/test.ps1 index 7a0c737a21..2dd318fe77 100644 --- a/build/test.ps1 +++ b/build/test.ps1 @@ -41,7 +41,7 @@ function Test-Python { Write-Host "##[info]Testing Python inside $testFolder" Push-Location (Join-Path $PSScriptRoot $testFolder) python --version - pytest -v + pytest -v --log-level=Debug Pop-Location if ($LastExitCode -ne 0) { diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index ae350d309f..cd4279a2bb 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -79,6 +79,7 @@ def test_workspace_create_with_resource_id(): assert len(targets) > 0 _test_workspace_with_providers_after_connection() + _test_workspace_job_execution() def test_workspace_create_with_resource_id_and_storage(): """ @@ -123,6 +124,7 @@ def _test_workspace_with_providers_after_connection(): assert isinstance(retrieved_job, AzureJob) assert job.id == retrieved_job.id +def _test_workspace_job_execution(): # Execute a workspace operation with parameters op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None) @@ -130,7 +132,7 @@ def _test_workspace_with_providers_after_connection(): qsharp.azure.execute(op) assert exception_info.value.error_name == "JobSubmissionFailed" - histogram = qsharp.azure.execute(op, count=3, name="test") + histogram = qsharp.azure.execute(op, count=3, name="test", timeout=3, poll=0.5) assert isinstance(histogram, dict) retrieved_histogram = qsharp.azure.output() From 7cccc7c0f8a6370c1f9599a6e3fe7b813b24a2db Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 17:39:17 -0700 Subject: [PATCH 09/19] Make NuGet.config available to conda package --- conda-recipes/iqsharp/build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index f08bdeb1ee..e13c8b82d1 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -17,8 +17,8 @@ if ($IsWindows) { $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); $ArtifactRoot = Join-Path $RepoRoot "drops"; $SelfContainedDirectory = Join-Path $ArtifactRoot (Join-Path "selfcontained" $RuntimeID) -$NugetsDirectory = Join-Path $ArtifactRoot "nugets" -$NugetConfig = Resolve-Path (Join-Path $PSScriptRoot "NuGet.config"); +$NugetConfig = Join-Path $RepoRoot "NuGet.config" +Copy-Item $NugetConfig $SelfContainedDirectory $TargetDirectory = (Join-Path (Join-Path $Env:PREFIX "opt") "iqsharp"); From 4effcf1ef87d54c18326834ed706e5099b234238 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 18:10:46 -0700 Subject: [PATCH 10/19] Add prerelease feed for non-release builds --- conda-recipes/iqsharp/build.ps1 | 2 -- conda-recipes/iqsharp/test.ps1 | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index e13c8b82d1..6c76741f98 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -17,8 +17,6 @@ if ($IsWindows) { $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); $ArtifactRoot = Join-Path $RepoRoot "drops"; $SelfContainedDirectory = Join-Path $ArtifactRoot (Join-Path "selfcontained" $RuntimeID) -$NugetConfig = Join-Path $RepoRoot "NuGet.config" -Copy-Item $NugetConfig $SelfContainedDirectory $TargetDirectory = (Join-Path (Join-Path $Env:PREFIX "opt") "iqsharp"); diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index e1f14d46f0..b15adda100 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -13,7 +13,14 @@ if ($null -eq $kernels.kernelspecs.iqsharp) { jupyter kernelspec list } +# Add the prerelease NuGet feed if this isn't a release build. +if ("$Env:BUILD_RELEASETYPE" -ne "release") { + $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); + $NugetConfig = Join-Path $RepoRoot "NuGet.config" + Copy-Item $NugetConfig (Join-Path (Resolve-Path ~) ".nuget/NuGet") +} +# Run the kernel unit tests. Push-Location $PSScriptRoot python test.py if ($LastExitCode -ne 0) { From d7ba3e1255d18a21182442cc747204dbe86be629 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 18:40:51 -0700 Subject: [PATCH 11/19] Copy NuGet config during build step --- conda-recipes/iqsharp/build.ps1 | 8 ++++++++ conda-recipes/iqsharp/test.ps1 | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index 6c76741f98..9033683df9 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -29,6 +29,14 @@ Get-ChildItem -Recurse $ArtifactRoot | %{ Write-Host $_.FullName } Write-Host "## Copying IQ# from '$SelfContainedDirectory' into '$TargetDirectory...' ##" Copy-Item (Join-Path $SelfContainedDirectory "*") $TargetDirectory -Verbose -Recurse -Force; +# Add the prerelease NuGet feed if this isn't a release build. +if ("$Env:BUILD_RELEASETYPE" -ne "release") { + $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); + $NuGetConfig = Join-Path $RepoRoot "NuGet.config" + Write-Host "## Copying prerelease NuGet config $NuGetConfig to $TargetDirectory ##" + Copy-Item $NuGetConfig $TargetDirectory +} + Write-Host "## Installing IQ# into Jupyter. ##" $BaseName = "Microsoft.Quantum.IQSharp"; if ($IsWindows) { diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index b15adda100..7cb4df343f 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -13,13 +13,6 @@ if ($null -eq $kernels.kernelspecs.iqsharp) { jupyter kernelspec list } -# Add the prerelease NuGet feed if this isn't a release build. -if ("$Env:BUILD_RELEASETYPE" -ne "release") { - $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); - $NugetConfig = Join-Path $RepoRoot "NuGet.config" - Copy-Item $NugetConfig (Join-Path (Resolve-Path ~) ".nuget/NuGet") -} - # Run the kernel unit tests. Push-Location $PSScriptRoot python test.py From 4f326ae29612ee133cd481f48623aa8c8380a576 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 19:07:28 -0700 Subject: [PATCH 12/19] Fix bad merge with job filter changes --- src/Jupyter/Magic/AbstractMagic.cs | 5 +++-- src/Python/qsharp/azure.py | 2 +- src/Python/qsharp/tests/test_azure.py | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index cf8866059a..d0c0e81d07 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -94,6 +94,8 @@ public static Dictionary ParseInputParameters(string input, stri var regex = new Regex(@"(\{.*\})|[^\s""]+(?:\s*=\s*)(?:""[^""]*""|[^\s""]*)*|[^\s""]+(?:""[^""]*""[^\s""]*)*|(?:""[^""]*""[^\s""]*)+"); var args = regex.Matches(input).Select(match => match.Value); + var regexBeginEndQuotes = @"^['""]|['""]$"; + // If we are expecting a first inferred-name parameter, see if it exists. // If so, serialize it to the dictionary as JSON and remove it from the list of args. if (args.Any() && @@ -102,7 +104,7 @@ public static Dictionary ParseInputParameters(string input, stri !string.IsNullOrEmpty(firstParameterInferredName)) { using var writer = new StringWriter(); - Json.Serializer.Serialize(writer, args.First()); + Json.Serializer.Serialize(writer, Regex.Replace(args.First(), regexBeginEndQuotes, string.Empty)); inputParameters[firstParameterInferredName] = writer.ToString(); args = args.Skip(1); } @@ -123,7 +125,6 @@ public static Dictionary ParseInputParameters(string input, stri foreach (string arg in args) { var tokens = arg.Split("=", 2); - var regexBeginEndQuotes = @"^['""]|['""]$"; var key = Regex.Replace(tokens[0].Trim(), regexBeginEndQuotes, string.Empty); var value = tokens.Length switch { diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index 04a7644998..32036f528a 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -131,6 +131,6 @@ def output(jobId : str = '', **params) -> Dict: return result def jobs(filter : str = '', **params) -> List[AzureJob]: - result = qsharp.client._execute_magic(f"azure.jobs {filter}", raise_on_stderr=False, **params) + result = qsharp.client._execute_magic(f"azure.jobs \"{filter}\"", raise_on_stderr=False, **params) if "error_code" in result: raise AzureError(result) return [AzureJob(job) for job in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index 66b91f80e2..13ba002958 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -145,7 +145,6 @@ def _test_workspace_job_execution(): assert len(jobs) == 2 # Check that job filtering works - jobs = qsharp.azure.jobs(job.id) - print(job.id) + jobs = qsharp.azure.jobs(jobs[0].id) assert isinstance(jobs, list) assert len(jobs) == 1 From fcb7588f77d086863809fe962c72ff98fbbf8c66 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 20:09:10 -0700 Subject: [PATCH 13/19] Copy NuGet.config to home folder --- build/steps-selfcontained.yml | 7 +++++++ conda-recipes/iqsharp/build.ps1 | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/build/steps-selfcontained.yml b/build/steps-selfcontained.yml index fc68c4a47b..60f9817ca8 100644 --- a/build/steps-selfcontained.yml +++ b/build/steps-selfcontained.yml @@ -29,6 +29,13 @@ steps: displayName: "Packing IQ# self-contained executable" workingDirectory: '$(System.DefaultWorkingDirectory)/build' +- task: CopyFiles@2 + displayName: 'Copy prerelease NuGet.config' + condition: and(succeededOrFailed(), ne(variables['Build.ReleaseType'], 'release')) + inputs: + Contents: NuGet.config + TargetFolder: '$(Build.ArtifactStagingDirectory)' + ## # Publish tests results and build artifacts. ## diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index 9033683df9..f03113013e 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -31,10 +31,10 @@ Copy-Item (Join-Path $SelfContainedDirectory "*") $TargetDirectory -Verbose -Rec # Add the prerelease NuGet feed if this isn't a release build. if ("$Env:BUILD_RELEASETYPE" -ne "release") { - $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); - $NuGetConfig = Join-Path $RepoRoot "NuGet.config" - Write-Host "## Copying prerelease NuGet config $NuGetConfig to $TargetDirectory ##" - Copy-Item $NuGetConfig $TargetDirectory + $NuGetConfig = Join-Path $SelfContainedDirectory "NuGet.config" + $NuGetDirectory = Resolve-Path ~ + Write-Host "## Copying prerelease NuGet config $NuGetConfig to $NuGetDirectory ##" + Copy-Item $NuGetConfig $NuGetDirectory } Write-Host "## Installing IQ# into Jupyter. ##" From 3891f08e9b338ee43a8da1f0bb13d6d5dcbb03b7 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 20:30:24 -0700 Subject: [PATCH 14/19] Copy NuGet.config in both jobs --- build/steps-selfcontained.yml | 2 +- build/steps.yml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/build/steps-selfcontained.yml b/build/steps-selfcontained.yml index 60f9817ca8..542b1a1aa8 100644 --- a/build/steps-selfcontained.yml +++ b/build/steps-selfcontained.yml @@ -33,7 +33,7 @@ steps: displayName: 'Copy prerelease NuGet.config' condition: and(succeededOrFailed(), ne(variables['Build.ReleaseType'], 'release')) inputs: - Contents: NuGet.config + Contents: '$(System.DefaultWorkingDirectory)/drops/NuGet.config' TargetFolder: '$(Build.ArtifactStagingDirectory)' ## diff --git a/build/steps.yml b/build/steps.yml index cbf77cecee..c3f29811f9 100644 --- a/build/steps.yml +++ b/build/steps.yml @@ -50,6 +50,13 @@ steps: testResultsFiles: '$(System.DefaultWorkingDirectory)/**/*.trx' testRunTitle: 'IQ# tests' +- task: CopyFiles@2 + displayName: 'Copy prerelease NuGet.config' + condition: and(succeededOrFailed(), ne(variables['Build.ReleaseType'], 'release')) + inputs: + Contents: '$(System.DefaultWorkingDirectory)/NuGet.config' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: iqsharp' condition: succeededOrFailed() From 39d7644015a11a48c050fefe9e52129923a93e38 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 21:12:02 -0700 Subject: [PATCH 15/19] Write prerelease NuGet directly --- build/steps-selfcontained.yml | 7 ------- build/steps.yml | 7 ------- conda-recipes/iqsharp/build.ps1 | 8 -------- conda-recipes/iqsharp/test.ps1 | 12 ++++++++++++ 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/build/steps-selfcontained.yml b/build/steps-selfcontained.yml index 542b1a1aa8..fc68c4a47b 100644 --- a/build/steps-selfcontained.yml +++ b/build/steps-selfcontained.yml @@ -29,13 +29,6 @@ steps: displayName: "Packing IQ# self-contained executable" workingDirectory: '$(System.DefaultWorkingDirectory)/build' -- task: CopyFiles@2 - displayName: 'Copy prerelease NuGet.config' - condition: and(succeededOrFailed(), ne(variables['Build.ReleaseType'], 'release')) - inputs: - Contents: '$(System.DefaultWorkingDirectory)/drops/NuGet.config' - TargetFolder: '$(Build.ArtifactStagingDirectory)' - ## # Publish tests results and build artifacts. ## diff --git a/build/steps.yml b/build/steps.yml index c3f29811f9..cbf77cecee 100644 --- a/build/steps.yml +++ b/build/steps.yml @@ -50,13 +50,6 @@ steps: testResultsFiles: '$(System.DefaultWorkingDirectory)/**/*.trx' testRunTitle: 'IQ# tests' -- task: CopyFiles@2 - displayName: 'Copy prerelease NuGet.config' - condition: and(succeededOrFailed(), ne(variables['Build.ReleaseType'], 'release')) - inputs: - Contents: '$(System.DefaultWorkingDirectory)/NuGet.config' - TargetFolder: '$(Build.ArtifactStagingDirectory)' - - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: iqsharp' condition: succeededOrFailed() diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index f03113013e..6c76741f98 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -29,14 +29,6 @@ Get-ChildItem -Recurse $ArtifactRoot | %{ Write-Host $_.FullName } Write-Host "## Copying IQ# from '$SelfContainedDirectory' into '$TargetDirectory...' ##" Copy-Item (Join-Path $SelfContainedDirectory "*") $TargetDirectory -Verbose -Recurse -Force; -# Add the prerelease NuGet feed if this isn't a release build. -if ("$Env:BUILD_RELEASETYPE" -ne "release") { - $NuGetConfig = Join-Path $SelfContainedDirectory "NuGet.config" - $NuGetDirectory = Resolve-Path ~ - Write-Host "## Copying prerelease NuGet config $NuGetConfig to $NuGetDirectory ##" - Copy-Item $NuGetConfig $NuGetDirectory -} - Write-Host "## Installing IQ# into Jupyter. ##" $BaseName = "Microsoft.Quantum.IQSharp"; if ($IsWindows) { diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index 7cb4df343f..889731c4e8 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -5,6 +5,18 @@ $failed = $false; $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" +# Add the prerelease NuGet feed if this isn't a release build. +if ("$Env:BUILD_RELEASETYPE" -ne "release") { + $NuGetDirectory = Resolve-Path ~ + Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" + echo " + + + + + " > $NuGetDirectory/NuGet.Config +} + # Check that iqsharp is installed as a Jupyter kernel. $kernels = jupyter kernelspec list --json | ConvertFrom-Json; if ($null -eq $kernels.kernelspecs.iqsharp) { From 6661e2361f4f1cc72b58d28b704348a4558cb2b5 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 21:41:10 -0700 Subject: [PATCH 16/19] Construct RegEx object once --- src/Jupyter/Magic/AbstractMagic.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index d0c0e81d07..5e53235dbe 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -94,7 +94,7 @@ public static Dictionary ParseInputParameters(string input, stri var regex = new Regex(@"(\{.*\})|[^\s""]+(?:\s*=\s*)(?:""[^""]*""|[^\s""]*)*|[^\s""]+(?:""[^""]*""[^\s""]*)*|(?:""[^""]*""[^\s""]*)+"); var args = regex.Matches(input).Select(match => match.Value); - var regexBeginEndQuotes = @"^['""]|['""]$"; + var regexBeginEndQuotes = new Regex(@"^['""]|['""]$"); // If we are expecting a first inferred-name parameter, see if it exists. // If so, serialize it to the dictionary as JSON and remove it from the list of args. @@ -104,7 +104,7 @@ public static Dictionary ParseInputParameters(string input, stri !string.IsNullOrEmpty(firstParameterInferredName)) { using var writer = new StringWriter(); - Json.Serializer.Serialize(writer, Regex.Replace(args.First(), regexBeginEndQuotes, string.Empty)); + Json.Serializer.Serialize(writer, regexBeginEndQuotes.Replace(args.First(), string.Empty)); inputParameters[firstParameterInferredName] = writer.ToString(); args = args.Skip(1); } @@ -125,14 +125,14 @@ public static Dictionary ParseInputParameters(string input, stri foreach (string arg in args) { var tokens = arg.Split("=", 2); - var key = Regex.Replace(tokens[0].Trim(), regexBeginEndQuotes, string.Empty); + var key = regexBeginEndQuotes.Replace(tokens[0].Trim(), string.Empty); var value = tokens.Length switch { // If there was no value provided explicitly, treat it as an implicit "true" value 1 => true as object, // Trim whitespace and also enclosing single-quotes or double-quotes before returning - 2 => Regex.Replace(tokens[1].Trim(), regexBeginEndQuotes, string.Empty) as object, + 2 => regexBeginEndQuotes.Replace(tokens[1].Trim(), string.Empty) as object, // We called arg.Split("=", 2), so there should never be more than 2 _ => throw new InvalidOperationException() From 3f5486c9332019ebaf70047809c07991500d78d2 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 21:45:35 -0700 Subject: [PATCH 17/19] Remove line breaks --- conda-recipes/iqsharp/test.ps1 | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index 889731c4e8..3007f27e56 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -9,12 +9,7 @@ $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" if ("$Env:BUILD_RELEASETYPE" -ne "release") { $NuGetDirectory = Resolve-Path ~ Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" - echo " - - - - - " > $NuGetDirectory/NuGet.Config + echo "" > $NuGetDirectory/NuGet.Config } # Check that iqsharp is installed as a Jupyter kernel. From 8a6db123f2fedf45534b83b2415023e594da080f Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 21:50:58 -0700 Subject: [PATCH 18/19] Write prerelease NuGet into target directory --- conda-recipes/iqsharp/build.ps1 | 7 +++++++ conda-recipes/iqsharp/test.ps1 | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index 6c76741f98..0dbb56b801 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -38,6 +38,13 @@ Push-Location $TargetDirectory $PathToTool = Resolve-Path "./$BaseName"; Write-Host "Path to IQ# kernel: $PathToTool"; + # Add the prerelease NuGet feed if this isn't a release build. + if ("$Env:BUILD_RELEASETYPE" -ne "release") { + $NuGetDirectory = Resolve-Path "." + Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" + echo "" > $NuGetDirectory/NuGet.Config + } + # If we're not on Windows, we need to make sure that the program is marked # as executable. if (-not $IsWindows) { diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index 3007f27e56..7cb4df343f 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -5,13 +5,6 @@ $failed = $false; $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" -# Add the prerelease NuGet feed if this isn't a release build. -if ("$Env:BUILD_RELEASETYPE" -ne "release") { - $NuGetDirectory = Resolve-Path ~ - Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" - echo "" > $NuGetDirectory/NuGet.Config -} - # Check that iqsharp is installed as a Jupyter kernel. $kernels = jupyter kernelspec list --json | ConvertFrom-Json; if ($null -eq $kernels.kernelspecs.iqsharp) { From 940284e71f69cfd0f79f28849269f64d71dcdda6 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 22:11:37 -0700 Subject: [PATCH 19/19] Put prerelease NuGet back in test.ps1 for now --- conda-recipes/iqsharp/build.ps1 | 7 ------- conda-recipes/iqsharp/test.ps1 | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index 0dbb56b801..6c76741f98 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -38,13 +38,6 @@ Push-Location $TargetDirectory $PathToTool = Resolve-Path "./$BaseName"; Write-Host "Path to IQ# kernel: $PathToTool"; - # Add the prerelease NuGet feed if this isn't a release build. - if ("$Env:BUILD_RELEASETYPE" -ne "release") { - $NuGetDirectory = Resolve-Path "." - Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" - echo "" > $NuGetDirectory/NuGet.Config - } - # If we're not on Windows, we need to make sure that the program is marked # as executable. if (-not $IsWindows) { diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index 7cb4df343f..3007f27e56 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -5,6 +5,13 @@ $failed = $false; $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" +# Add the prerelease NuGet feed if this isn't a release build. +if ("$Env:BUILD_RELEASETYPE" -ne "release") { + $NuGetDirectory = Resolve-Path ~ + Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" + echo "" > $NuGetDirectory/NuGet.Config +} + # Check that iqsharp is installed as a Jupyter kernel. $kernels = jupyter kernelspec list --json | ConvertFrom-Json; if ($null -eq $kernels.kernelspecs.iqsharp) {