From d43f74b4159524a5e817fd3df4809bc33cd728bd Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 17 Apr 2020 14:35:40 -0700 Subject: [PATCH 01/13] Add IAzureClient definition and initial magic commands --- src/AzureClient/AzureClient.cs | 72 ++++++++++- src/AzureClient/Extensions.cs | 43 ++++++- src/AzureClient/IAzureClient.cs | 94 +++++++++++++- src/AzureClient/Magic/ConnectMagic.cs | 107 ++++++++++++++++ src/AzureClient/Magic/StatusMagic.cs | 82 ++++++++++++ src/AzureClient/Magic/SubmitMagic.cs | 78 ++++++++++++ src/AzureClient/Magic/TargetMagic.cs | 82 ++++++++++++ src/Tests/AzureClientTests.cs | 171 +++++++++++++++++++++++++- src/Tests/IQsharpEngineTests.cs | 7 ++ src/Tests/Mocks.cs | 8 ++ src/Tests/Startup.cs | 2 + 11 files changed, 741 insertions(+), 5 deletions(-) create mode 100644 src/AzureClient/Magic/ConnectMagic.cs create mode 100644 src/AzureClient/Magic/StatusMagic.cs create mode 100644 src/AzureClient/Magic/SubmitMagic.cs create mode 100644 src/AzureClient/Magic/TargetMagic.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 3d941f5233..8f2160bae5 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #nullable enable @@ -20,5 +20,75 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { + /// + /// Creates an AzureClient object. + /// + public AzureClient() + { + } + + /// + public async Task ConnectAsync( + IChannel channel, + string subscriptionId, + string resourceGroupName, + string workspaceName, + string storageAccountConnectionString, + bool forceLogin = false) + { + return AzureClientError.UnknownError; + } + + /// + public async Task PrintConnectionStatusAsync(IChannel channel) + { + return AzureClientError.UnknownError; + } + + /// + public async Task SubmitJobAsync( + IChannel channel, + IOperationResolver operationResolver, + string operationName) + { + return AzureClientError.UnknownError; + } + + /// + public async Task SetActiveTargetAsync( + IChannel channel, + string targetName) + { + return AzureClientError.UnknownError; + } + + /// + public async Task PrintActiveTargetAsync( + IChannel channel) + { + return AzureClientError.UnknownError; + } + + /// + public async Task PrintTargetListAsync( + IChannel channel) + { + return AzureClientError.UnknownError; + } + + /// + public async Task PrintJobStatusAsync( + IChannel channel, + string jobId) + { + return AzureClientError.UnknownError; + } + + /// + public async Task PrintJobListAsync( + IChannel channel) + { + return AzureClientError.UnknownError; + } } } diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index fbfe0a4a8e..6a53dd7376 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #nullable enable @@ -6,7 +6,9 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Jupyter.Core; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -22,5 +24,44 @@ public static void AddAzureClient(this IServiceCollection services) { services.AddSingleton(); } + + /// + /// Parses the input parameters for a given magic symbol and returns a + /// Dictionary with the names and values of the parameters. + /// + public static Dictionary ParseInput(this MagicSymbol magic, string input) + { + Dictionary keyValuePairs = new Dictionary(); + foreach (string arg in input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries)) + { + var tokens = arg.Split("=", 2); + var key = tokens[0].Trim(); + var value = (tokens.Length == 1) ? string.Empty : tokens[1].Trim(); + keyValuePairs[key] = value; + } + return keyValuePairs; + } + + /// + /// Encapsulates a given AzureClientError as the result of an execution. + /// + /// + /// The result of an IAzureClient API call. + /// + public static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => + new ExecutionResult + { + Status = azureClientError == AzureClientError.Success ? ExecuteStatus.Ok : ExecuteStatus.Error, + Output = azureClientError + }; + + /// + /// Encapsulates a given AzureClientError as the result of an execution. + /// + /// + /// A task which will return the result of an IAzureClient API call. + /// + public static async Task ToExecutionResult(this Task task) => + (await task).ToExecutionResult(); } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 8efd69e5a7..84ee4b3fd0 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #nullable enable @@ -11,11 +11,103 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { + /// + /// Describes possible error results from IAzureClient methods. + /// + public enum AzureClientError + { + /// + /// Method completed successfully. + /// + Success = 0, + + /// + /// Method completed with an unknown error. + /// + UnknownError = 9999, + + /// + /// No connection has been made to any Azure Quantum workspace. + /// + NoWorkspace = 1, + + /// + /// A target has not yet been configured for job submission. + /// + NoTarget = 2, + + /// + /// A job meeting the specified criteria was not found. + /// + JobNotFound = 3, + + /// + /// No Q# operation name was provided where one was required. + /// + NoOperationName = 4, + } + /// /// This service is capable of connecting to Azure Quantum workspaces /// and submitting jobs. /// public interface IAzureClient { + /// + /// Connects to the specified Azure Quantum workspace, first logging into Azure if necessary. + /// + public Task ConnectAsync( + IChannel channel, + string subscriptionId, + string resourceGroupName, + string workspaceName, + string storageAccountConnectionString, + bool forceLogin = false); + + /// + /// Prints a string describing the current connection status. + /// + public Task PrintConnectionStatusAsync( + IChannel channel); + + /// + /// Submits the specified Q# operation as a job to the currently active target. + /// + public Task SubmitJobAsync( + IChannel channel, + IOperationResolver operationResolver, + string operationName); + + /// + /// Sets the specified target for job submission. + /// + public Task SetActiveTargetAsync( + IChannel channel, + string targetName); + + /// + /// Prints the specified target for job submission. + /// + public Task PrintActiveTargetAsync( + IChannel channel); + + /// + /// Prints the list of targets currently provisioned in the current workspace. + /// + public Task PrintTargetListAsync( + IChannel channel); + + /// + /// Prints the job status corresponding to the given job ID. + /// + public Task PrintJobStatusAsync( + IChannel channel, + string jobId); + + /// + /// Prints a list of all jobs in the current workspace. + /// + public Task PrintJobListAsync( + IChannel channel); } } diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs new file mode 100644 index 0000000000..1e2302827b --- /dev/null +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to connect to an Azure workspace. + /// + public class ConnectMagic : MagicSymbol + { + /// + /// The object used by this magic command to interact with Azure. + /// + public IAzureClient AzureClient { get; } + + /// + /// Constructs a new magic command given an IAzureClient object. + /// + public ConnectMagic(IAzureClient azureClient) + { + this.AzureClient = azureClient; + + this.Name = "%connect"; + this.Kind = SymbolKind.Magic; + this.Execute = async(input, channel) => await RunAsync(input, channel); + this.Documentation = new Documentation + { + Summary = "Connects to an Azure workspace or displays current connection status.", + Description = @" + This magic command allows for connecting to an Azure Quantum workspace + as specified by a valid connection string OR a valid combination of + subscription ID, resource group name, and workspace name. + ".Dedent(), + Examples = new[] + { + @" + Print information about the current connection: + ``` + In []: %connect + Out[]: Connected to WORKSPACE_NAME + ``` + ".Dedent(), + + @" + Connect to an Azure Quantum workspace using a connection string: + ``` + In []: %connect connectionString=CONNECTION_STRING + Out[]: Connected to WORKSPACE_NAME + ``` + ".Dedent(), + + @" + Connect to an Azure Quantum workspace and force a credential prompt: + ``` + In []: %connect login connectionString=CONNECTION_STRING + 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 WORKSPACE_NAME + ``` + Use the `login` option if you want to bypass any saved or cached + credentials when connecting to Azure. + ".Dedent() + } + }; + } + + /// + /// Connects to an Azure workspace given a subscription ID, resource group name, + /// workspace name, and connection string as a JSON-encoded object. + /// + public async Task RunAsync(string input, IChannel channel) + { + channel = channel.WithNewLines(); + + Dictionary keyValuePairs = this.ParseInput(input); + + string storageAccountConnectionString; + keyValuePairs.TryGetValue("storageAccountConnectionString", out storageAccountConnectionString); + if (string.IsNullOrEmpty(storageAccountConnectionString)) + { + return await AzureClient.PrintConnectionStatusAsync(channel).ToExecutionResult(); + } + + string subscriptionId, resourceGroupName, workspaceName; + keyValuePairs.TryGetValue("subscriptionId", out subscriptionId); + keyValuePairs.TryGetValue("resourceGroupName", out resourceGroupName); + keyValuePairs.TryGetValue("workspaceName", out workspaceName); + + bool forceLogin = keyValuePairs.ContainsKey("login"); + return await AzureClient.ConnectAsync( + channel, + subscriptionId, + resourceGroupName, + workspaceName, + storageAccountConnectionString, + forceLogin).ToExecutionResult(); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs new file mode 100644 index 0000000000..be41814d65 --- /dev/null +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to connect to an Azure workspace. + /// + public class StatusMagic : MagicSymbol + { + /// + /// The object used by this magic command to interact with Azure. + /// + public IAzureClient AzureClient { get; } + + /// + /// Constructs a new magic command given an IAzureClient object. + /// + public StatusMagic(IAzureClient azureClient) + { + this.AzureClient = azureClient; + + this.Name = "%status"; + this.Kind = SymbolKind.Magic; + this.Execute = async (input, channel) => await RunAsync(input, channel); + this.Documentation = new Documentation + { + Summary = "Displays status for jobs in the current Azure Quantum workspace.", + Description = @" + This magic command allows for displaying status of jobs in the current + Azure Quantum workspace. If a valid job ID is provided as an argument, the + detailed status of that job will be displayed; otherwise, a list of jobs + in the current workspace will be displayed. + ".Dedent(), + Examples = new[] + { + @" + Print status about a specific job: + ``` + In []: %status JOB_ID + Out[]: JOB_ID: + ``` + ".Dedent(), + + @" + Print status about all jobs in the current Azure Quantum workspace: + ``` + In []: %status + Out[]: + ``` + ".Dedent() + } + }; + } + + /// + /// Displays the status corresponding to a given job ID, if provided, + /// or all jobs in the active workspace. + /// + public async Task RunAsync(string input, IChannel channel) + { + channel = channel.WithNewLines(); + + Dictionary keyValuePairs = this.ParseInput(input); + if (keyValuePairs.Keys.Count > 0) + { + var jobId = keyValuePairs.Keys.First(); + return await AzureClient.PrintJobStatusAsync(channel, jobId).ToExecutionResult(); + } + + return await AzureClient.PrintJobListAsync(channel).ToExecutionResult(); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs new file mode 100644 index 0000000000..1a1d50cf8d --- /dev/null +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to submit jobs to an Azure Quantum workspace. + /// + public class SubmitMagic : MagicSymbol + { + /// + /// Constructs a new magic command given a resolver used to find + /// operations and functions and an IAzureClient object. + /// + public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) + { + this.OperationResolver = operationResolver; + this.AzureClient = azureClient; + + this.Name = "%submit"; + this.Kind = SymbolKind.Magic; + this.Execute = async (input, channel) => await RunAsync(input, channel); + this.Documentation = new Documentation + { + Summary = "Submits a job to an Azure Quantum workspace.", + Description = @" + This magic command allows for submitting a job to an Azure Quantum workspace + corresponding to the Q# operation provided as an argument. + + The Azure Quantum workspace must previously have been initialized + using the %connect magic command. + ".Dedent(), + Examples = new[] + { + @" + Submit an operation as a new job to the current Azure Quantum workspace: + ``` + In []: %submit OPERATION_NAME + Out[]: Submitted job JOB_ID + ``` + ".Dedent(), + } + }; + } + + /// + /// The symbol resolver used by this magic command to find + /// operations or functions to be simulated. + /// + public IOperationResolver OperationResolver { get; } + + /// + /// The object used by this magic command to interact with Azure. + /// + public IAzureClient AzureClient { get; } + + /// + /// Submits a new job to an Azure Quantum workspace given a Q# operation + /// name that is present in the current Q# Jupyter workspace. + /// + public async Task RunAsync(string input, IChannel channel) + { + channel = channel.WithNewLines(); + + Dictionary keyValuePairs = this.ParseInput(input); + var operationName = keyValuePairs.Keys.FirstOrDefault(); + return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName).ToExecutionResult(); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs new file mode 100644 index 0000000000..88c2d4a2bc --- /dev/null +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to view or set target information for an Azure Quantum workspace. + /// + public class TargetMagic : MagicSymbol + { + /// + /// Constructs a new magic command given an IAzureClient object. + /// + public TargetMagic(IAzureClient azureClient) + { + this.AzureClient = azureClient; + + this.Name = "%target"; + this.Kind = SymbolKind.Magic; + this.Execute = async (input, channel) => await RunAsync(input, channel); + this.Documentation = new Documentation + { + Summary = "Views or sets the target for job submission to an Azure Quantum workspace.", + Description = @" + This magic command allows for specifying a target for job submission + to an Azure Quantum workspace, or viewing the list of all available targets. + + The Azure Quantum workspace must previously have been initialized + using the %connect magic command, and the specified target must be + available in the workspace. + ".Dedent(), + Examples = new[] + { + @" + Set the current target for job submission: + ``` + In []: %target TARGET_NAME + Out[]: Active target is now TARGET_NAME + ``` + ".Dedent(), + @" + View the current target and all available targets in the current Azure Quantum workspace: + ``` + In []: %target + Out[]: + ``` + ".Dedent(), + } + }; + } + + /// + /// The object used by this magic command to interact with Azure. + /// + public IAzureClient AzureClient { get; } + + /// + /// Sets or views the target for job submission to the current Azure Quantum workspace. + /// + public async Task RunAsync(string input, IChannel channel) + { + channel = channel.WithNewLines(); + + Dictionary keyValuePairs = this.ParseInput(input); + if (keyValuePairs.Keys.Count > 0) + { + var targetName = keyValuePairs.Keys.First(); + return await AzureClient.SetActiveTargetAsync(channel, targetName).ToExecutionResult(); + } + + return await AzureClient.PrintTargetListAsync(channel).ToExecutionResult(); + } + } +} \ No newline at end of file diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index ba18985f1c..a286496270 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -7,16 +7,183 @@ using Microsoft.Quantum.IQSharp.AzureClient; using System.Linq; using System.Collections.Generic; +using Microsoft.Jupyter.Core; +using System.Threading.Tasks; +using Microsoft.Quantum.IQSharp; namespace Tests.IQSharp { + public static class AzureClientTestExtensions + { + public static void Test(this MagicSymbol magic, string input, ExecuteStatus expected = ExecuteStatus.Ok) + { + var result = magic.Execute(input, new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == expected); + } + } + [TestClass] public class AzureClientTests { + private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; + private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; + private readonly string workspaceName = "TEST_WORKSPACE_NAME"; + private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; + private readonly string jobId = "TEST_JOB_ID"; + private readonly string operationName = "TEST_OPERATION_NAME"; + private readonly string targetName = "TEST_TARGET_NAME"; + + [TestMethod] + public void TestConnectMagic() + { + var azureClient = new MockAzureClient(); + var connectMagic = new ConnectMagic(azureClient); + + // unrecognized input + connectMagic.Test($"invalid"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintConnectionStatus); + + // valid input + connectMagic.Test( + @$"subscriptionId={subscriptionId} + resourceGroupName={resourceGroupName} + workspaceName={workspaceName} + storageAccountConnectionString={storageAccountConnectionString}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.Connect); + Assert.IsFalse(azureClient.ForceLogin); + Assert.AreEqual(azureClient.ConnectionString, storageAccountConnectionString); + + // valid input with forced login + connectMagic.Test( + @$"login + subscriptionId={subscriptionId} + resourceGroupName={resourceGroupName} + workspaceName={workspaceName} + storageAccountConnectionString={storageAccountConnectionString}"); + + Assert.IsTrue(azureClient.ForceLogin); + } + [TestMethod] - public void TestNothing() + public void TestStatusMagic() + { + // no arguments - should print job list + var azureClient = new MockAzureClient(); + var statusMagic = new StatusMagic(azureClient); + statusMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobList); + + // single argument - should print job status + azureClient = new MockAzureClient(); + statusMagic = new StatusMagic(azureClient); + statusMagic.Test($"{jobId}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobStatus); + } + + [TestMethod] + public void TestSubmitMagic() + { + // no arguments + var azureClient = new MockAzureClient(); + var operationResolver = new MockOperationResolver(); + var submitMagic = new SubmitMagic(operationResolver, azureClient); + submitMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + + // single argument + submitMagic.Test($"{operationName}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + Assert.IsTrue(azureClient.SubmittedJobs.Contains(operationName)); + } + + [TestMethod] + public void TestTargetMagic() + { + // no arguments - should print target list + var azureClient = new MockAzureClient(); + var targetMagic = new TargetMagic(azureClient); + targetMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintTargetList); + + // single argument - should set active target + azureClient = new MockAzureClient(); + targetMagic = new TargetMagic(azureClient); + targetMagic.Test(targetName); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); + } + } + + internal enum AzureClientAction + { + None, + Connect, + SetActiveTarget, + SubmitJob, + PrintActiveTarget, + PrintConnectionStatus, + PrintJobList, + PrintJobStatus, + PrintTargetList, + } + + public class MockAzureClient : IAzureClient + { + internal AzureClientAction LastAction = AzureClientAction.None; + internal string ConnectionString = string.Empty; + internal bool ForceLogin = false; + internal string ActiveTargetName = string.Empty; + internal List SubmittedJobs = new List(); + + public async Task SetActiveTargetAsync(IChannel channel, string targetName) + { + LastAction = AzureClientAction.SetActiveTarget; + ActiveTargetName = targetName; + return AzureClientError.Success; + } + + public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + { + LastAction = AzureClientAction.SubmitJob; + SubmittedJobs.Add(operationName); + return AzureClientError.Success; + } + + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool forceLogin) + { + LastAction = AzureClientAction.Connect; + ConnectionString = storageAccountConnectionString; + ForceLogin = forceLogin; + return AzureClientError.Success; + } + + public async Task PrintActiveTargetAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintActiveTarget; + return AzureClientError.Success; + } + + public async Task PrintConnectionStatusAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintConnectionStatus; + return AzureClientError.Success; + } + + public async Task PrintJobListAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintJobList; + return AzureClientError.Success; + } + + public async Task PrintJobStatusAsync(IChannel channel, string jobId) + { + LastAction = AzureClientAction.PrintJobStatus; + return AzureClientError.Success; + } + + public async Task PrintTargetListAsync(IChannel channel) { - Assert.IsTrue(true); + LastAction = AzureClientAction.PrintTargetList; + return AzureClientError.Success; } } } diff --git a/src/Tests/IQsharpEngineTests.cs b/src/Tests/IQsharpEngineTests.cs index bd9d9b0f64..a4449ada95 100644 --- a/src/Tests/IQsharpEngineTests.cs +++ b/src/Tests/IQsharpEngineTests.cs @@ -15,6 +15,7 @@ using Newtonsoft.Json; using System.Data; +using Microsoft.Quantum.IQSharp.AzureClient; #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods @@ -447,6 +448,12 @@ public void TestResolveMagic() symbol = resolver.Resolve("%foo"); Assert.IsNull(symbol); + + // AzureClient-provided commands + Assert.IsNotNull(resolver.Resolve("%connect")); + Assert.IsNotNull(resolver.Resolve("%status")); + Assert.IsNotNull(resolver.Resolve("%submit")); + Assert.IsNotNull(resolver.Resolve("%target")); } } } diff --git a/src/Tests/Mocks.cs b/src/Tests/Mocks.cs index 11fcb4f645..84d06abe64 100644 --- a/src/Tests/Mocks.cs +++ b/src/Tests/Mocks.cs @@ -117,4 +117,12 @@ public IUpdatableDisplay DisplayUpdatable(object displayable) public void Stdout(string message) => msgs.Add(message); } + + public class MockOperationResolver : IOperationResolver + { + public OperationInfo Resolve(string input) + { + return new OperationInfo(null, null); + } + } } diff --git a/src/Tests/Startup.cs b/src/Tests/Startup.cs index c370d67d10..bc6ac12d15 100644 --- a/src/Tests/Startup.cs +++ b/src/Tests/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; using Microsoft.Quantum.IQSharp.Kernel; namespace Tests.IQSharp @@ -34,6 +35,7 @@ internal static ServiceProvider CreateServiceProvider(string workspaceFolder) services.AddTelemetry(); services.AddIQSharp(); services.AddIQSharpKernel(); + services.AddAzureClient(); var serviceProvider = services.BuildServiceProvider(); serviceProvider.GetRequiredService(); From fb1b23d1ad20ee24a23d45881c0b433d37c26f4e Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 4 May 2020 15:57:28 -0400 Subject: [PATCH 02/13] Add AzureClient assembly to manifest --- build/manifest.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/build/manifest.ps1 b/build/manifest.ps1 index d767d026f6..9aaeea1fb1 100644 --- a/build/manifest.ps1 +++ b/build/manifest.ps1 @@ -11,6 +11,7 @@ ); Assemblies = @( ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.dll", + ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.AzureClient.dll", ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Core.dll", ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Jupyter.dll", ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Kernel.dll", From 328b64d013139a3cc0509dcf3cca683d45792b96 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 22 Apr 2020 15:42:30 -0400 Subject: [PATCH 03/13] Add base class with common functionality for AzureClient magic commands --- src/AzureClient/Extensions.cs | 17 ---- src/AzureClient/Magic/AzureClientMagicBase.cs | 87 ++++++++++++++++ src/AzureClient/Magic/ConnectMagic.cs | 98 ++++++++----------- src/AzureClient/Magic/StatusMagic.cs | 76 ++++++-------- src/AzureClient/Magic/SubmitMagic.cs | 72 ++++++-------- src/AzureClient/Magic/TargetMagic.cs | 75 +++++++------- 6 files changed, 226 insertions(+), 199 deletions(-) create mode 100644 src/AzureClient/Magic/AzureClientMagicBase.cs diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 6a53dd7376..478121cece 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -25,23 +25,6 @@ public static void AddAzureClient(this IServiceCollection services) services.AddSingleton(); } - /// - /// Parses the input parameters for a given magic symbol and returns a - /// Dictionary with the names and values of the parameters. - /// - public static Dictionary ParseInput(this MagicSymbol magic, string input) - { - Dictionary keyValuePairs = new Dictionary(); - foreach (string arg in input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries)) - { - var tokens = arg.Split("=", 2); - var key = tokens[0].Trim(); - var value = (tokens.Length == 1) ? string.Empty : tokens[1].Trim(); - keyValuePairs[key] = value; - } - return keyValuePairs; - } - /// /// Encapsulates a given AzureClientError as the result of an execution. /// diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs new file mode 100644 index 0000000000..06dd581aca --- /dev/null +++ b/src/AzureClient/Magic/AzureClientMagicBase.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Common; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Base class used for Azure Client magic commands. + /// + public abstract class AzureClientMagicBase : MagicSymbol + { + /// + /// The object used by this magic command to interact with Azure. + /// + public IAzureClient AzureClient { get; } + + /// + /// Constructs the Azure Client magic command with the specified keyword + /// and documentation. + /// + public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentation docs) + { + this.AzureClient = azureClient; + this.Name = $"%{keyword}"; + this.Documentation = docs; + + this.Kind = SymbolKind.Magic; + this.Execute = SafeExecute(this.RunAsync); + } + + /// + /// Performs Azure Client magic commands with safe exception handling + /// and translates the result from AzureClient.AzureClientError + /// to Jupyter.Core.ExecutionResult. + /// + public Func> SafeExecute(Func> azureClientMagic) => + async (input, channel) => + { + channel = channel.WithNewLines(); + + try + { + return await azureClientMagic(input, channel).ToExecutionResult(); + } + catch (InvalidWorkspaceException ws) + { + foreach (var m in ws.Errors) channel.Stderr(m); + return ExecuteStatus.Error.ToExecutionResult(); + } + catch (AggregateException agg) + { + foreach (var e in agg.InnerExceptions) channel.Stderr(e?.Message); + return ExecuteStatus.Error.ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr(e.Message); + return ExecuteStatus.Error.ToExecutionResult(); + } + }; + + /// + /// Parses the input parameters for a given magic symbol and returns a + /// Dictionary with the names and values of the parameters. + /// + public Dictionary ParseInput(string input) + { + Dictionary keyValuePairs = new Dictionary(); + foreach (string arg in input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries)) + { + var tokens = arg.Split("=", 2); + var key = tokens[0].Trim(); + var value = (tokens.Length == 1) ? string.Empty : tokens[1].Trim(); + keyValuePairs[key] = value; + } + return keyValuePairs; + } + + /// + /// Executes the magic command functionality for the given input. + /// + public abstract Task RunAsync(string input, IChannel channel); + } +} diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 1e2302827b..bff4d75e86 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -14,79 +14,67 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// /// A magic command that can be used to connect to an Azure workspace. /// - public class ConnectMagic : MagicSymbol + public class ConnectMagic : AzureClientMagicBase { - /// - /// The object used by this magic command to interact with Azure. - /// - public IAzureClient AzureClient { get; } - /// /// Constructs a new magic command given an IAzureClient object. /// - public ConnectMagic(IAzureClient azureClient) - { - this.AzureClient = azureClient; - - this.Name = "%connect"; - this.Kind = SymbolKind.Magic; - this.Execute = async(input, channel) => await RunAsync(input, channel); - this.Documentation = new Documentation - { - Summary = "Connects to an Azure workspace or displays current connection status.", - Description = @" - This magic command allows for connecting to an Azure Quantum workspace - as specified by a valid connection string OR a valid combination of - subscription ID, resource group name, and workspace name. - ".Dedent(), - Examples = new[] + public ConnectMagic(IAzureClient azureClient) : + base(azureClient, + "connect", + new Documentation { - @" - Print information about the current connection: - ``` - In []: %connect - Out[]: Connected to WORKSPACE_NAME - ``` - ".Dedent(), + Summary = "Connects to an Azure workspace or displays current connection status.", + Description = @" + This magic command allows for connecting to an Azure Quantum workspace + as specified by a valid connection string OR a valid combination of + subscription ID, resource group name, and workspace name. + ".Dedent(), + Examples = new[] + { + @" + Print information about the current connection: + ``` + In []: %connect + Out[]: Connected to WORKSPACE_NAME + ``` + ".Dedent(), - @" - Connect to an Azure Quantum workspace using a connection string: - ``` - In []: %connect connectionString=CONNECTION_STRING - Out[]: Connected to WORKSPACE_NAME - ``` - ".Dedent(), + @" + Connect to an Azure Quantum workspace using a connection string: + ``` + In []: %connect connectionString=CONNECTION_STRING + Out[]: Connected to WORKSPACE_NAME + ``` + ".Dedent(), - @" - Connect to an Azure Quantum workspace and force a credential prompt: - ``` - In []: %connect login connectionString=CONNECTION_STRING - 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 WORKSPACE_NAME - ``` - Use the `login` option if you want to bypass any saved or cached - credentials when connecting to Azure. - ".Dedent() - } - }; - } + @" + Connect to an Azure Quantum workspace and force a credential prompt: + ``` + In []: %connect login connectionString=CONNECTION_STRING + 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 WORKSPACE_NAME + ``` + Use the `login` option if you want to bypass any saved or cached + credentials when connecting to Azure. + ".Dedent() + } + }) {} /// /// Connects to an Azure workspace given a subscription ID, resource group name, /// workspace name, and connection string as a JSON-encoded object. /// - public async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { - channel = channel.WithNewLines(); - Dictionary keyValuePairs = this.ParseInput(input); string storageAccountConnectionString; keyValuePairs.TryGetValue("storageAccountConnectionString", out storageAccountConnectionString); if (string.IsNullOrEmpty(storageAccountConnectionString)) { - return await AzureClient.PrintConnectionStatusAsync(channel).ToExecutionResult(); + return await AzureClient.PrintConnectionStatusAsync(channel); } string subscriptionId, resourceGroupName, workspaceName; @@ -101,7 +89,7 @@ public async Task RunAsync(string input, IChannel channel) resourceGroupName, workspaceName, storageAccountConnectionString, - forceLogin).ToExecutionResult(); + forceLogin); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index be41814d65..474eac4a2c 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -14,69 +14,57 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// /// A magic command that can be used to connect to an Azure workspace. /// - public class StatusMagic : MagicSymbol + public class StatusMagic : AzureClientMagicBase { - /// - /// The object used by this magic command to interact with Azure. - /// - public IAzureClient AzureClient { get; } - /// /// Constructs a new magic command given an IAzureClient object. /// - public StatusMagic(IAzureClient azureClient) - { - this.AzureClient = azureClient; - - this.Name = "%status"; - this.Kind = SymbolKind.Magic; - this.Execute = async (input, channel) => await RunAsync(input, channel); - this.Documentation = new Documentation - { - Summary = "Displays status for jobs in the current Azure Quantum workspace.", - Description = @" - This magic command allows for displaying status of jobs in the current - Azure Quantum workspace. If a valid job ID is provided as an argument, the - detailed status of that job will be displayed; otherwise, a list of jobs - in the current workspace will be displayed. - ".Dedent(), - Examples = new[] + public StatusMagic(IAzureClient azureClient) : + base(azureClient, + "status", + new Documentation { - @" - Print status about a specific job: - ``` - In []: %status JOB_ID - Out[]: JOB_ID: - ``` + Summary = "Displays status for jobs in the current Azure Quantum workspace.", + Description = @" + This magic command allows for displaying status of jobs in the current + Azure Quantum workspace. If a valid job ID is provided as an argument, the + detailed status of that job will be displayed; otherwise, a list of jobs + in the current workspace will be displayed. ".Dedent(), + Examples = new[] + { + @" + Print status about a specific job: + ``` + In []: %status JOB_ID + Out[]: JOB_ID: + ``` + ".Dedent(), - @" - Print status about all jobs in the current Azure Quantum workspace: - ``` - In []: %status - Out[]: - ``` - ".Dedent() - } - }; - } + @" + Print status about all jobs in the current Azure Quantum workspace: + ``` + In []: %status + Out[]: + ``` + ".Dedent() + } + }) {} /// /// Displays the status corresponding to a given job ID, if provided, /// or all jobs in the active workspace. /// - public async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { - channel = channel.WithNewLines(); - Dictionary keyValuePairs = this.ParseInput(input); if (keyValuePairs.Keys.Count > 0) { var jobId = keyValuePairs.Keys.First(); - return await AzureClient.PrintJobStatusAsync(channel, jobId).ToExecutionResult(); + return await AzureClient.PrintJobStatusAsync(channel, jobId); } - return await AzureClient.PrintJobListAsync(channel).ToExecutionResult(); + return await AzureClient.PrintJobListAsync(channel); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 1a1d50cf8d..59c9e7994f 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -14,43 +14,8 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// /// A magic command that can be used to submit jobs to an Azure Quantum workspace. /// - public class SubmitMagic : MagicSymbol + public class SubmitMagic : AzureClientMagicBase { - /// - /// Constructs a new magic command given a resolver used to find - /// operations and functions and an IAzureClient object. - /// - public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) - { - this.OperationResolver = operationResolver; - this.AzureClient = azureClient; - - this.Name = "%submit"; - this.Kind = SymbolKind.Magic; - this.Execute = async (input, channel) => await RunAsync(input, channel); - this.Documentation = new Documentation - { - Summary = "Submits a job to an Azure Quantum workspace.", - Description = @" - This magic command allows for submitting a job to an Azure Quantum workspace - corresponding to the Q# operation provided as an argument. - - The Azure Quantum workspace must previously have been initialized - using the %connect magic command. - ".Dedent(), - Examples = new[] - { - @" - Submit an operation as a new job to the current Azure Quantum workspace: - ``` - In []: %submit OPERATION_NAME - Out[]: Submitted job JOB_ID - ``` - ".Dedent(), - } - }; - } - /// /// The symbol resolver used by this magic command to find /// operations or functions to be simulated. @@ -58,21 +23,44 @@ The Azure Quantum workspace must previously have been initialized public IOperationResolver OperationResolver { get; } /// - /// The object used by this magic command to interact with Azure. + /// Constructs a new magic command given a resolver used to find + /// operations and functions and an IAzureClient object. /// - public IAzureClient AzureClient { get; } + public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) : + base(azureClient, + "submit", + new Documentation + { + Summary = "Submits a job to an Azure Quantum workspace.", + Description = @" + This magic command allows for submitting a job to an Azure Quantum workspace + corresponding to the Q# operation provided as an argument. + + The Azure Quantum workspace must previously have been initialized + using the %connect magic command. + ".Dedent(), + Examples = new[] + { + @" + Submit an operation as a new job to the current Azure Quantum workspace: + ``` + In []: %submit OPERATION_NAME + Out[]: Submitted job JOB_ID + ``` + ".Dedent(), + } + }) => + this.OperationResolver = operationResolver; /// /// Submits a new job to an Azure Quantum workspace given a Q# operation /// name that is present in the current Q# Jupyter workspace. /// - public async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { - channel = channel.WithNewLines(); - Dictionary keyValuePairs = this.ParseInput(input); var operationName = keyValuePairs.Keys.FirstOrDefault(); - return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName).ToExecutionResult(); + return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 88c2d4a2bc..8a47b0e9ae 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -14,48 +14,43 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// /// A magic command that can be used to view or set target information for an Azure Quantum workspace. /// - public class TargetMagic : MagicSymbol + public class TargetMagic : AzureClientMagicBase { /// /// Constructs a new magic command given an IAzureClient object. /// - public TargetMagic(IAzureClient azureClient) - { - this.AzureClient = azureClient; - - this.Name = "%target"; - this.Kind = SymbolKind.Magic; - this.Execute = async (input, channel) => await RunAsync(input, channel); - this.Documentation = new Documentation - { - Summary = "Views or sets the target for job submission to an Azure Quantum workspace.", - Description = @" - This magic command allows for specifying a target for job submission - to an Azure Quantum workspace, or viewing the list of all available targets. - - The Azure Quantum workspace must previously have been initialized - using the %connect magic command, and the specified target must be - available in the workspace. - ".Dedent(), - Examples = new[] + public TargetMagic(IAzureClient azureClient) : + base(azureClient, + "target", + new Documentation { - @" - Set the current target for job submission: - ``` - In []: %target TARGET_NAME - Out[]: Active target is now TARGET_NAME - ``` - ".Dedent(), - @" - View the current target and all available targets in the current Azure Quantum workspace: - ``` - In []: %target - Out[]: - ``` + Summary = "Views or sets the target for job submission to an Azure Quantum workspace.", + Description = @" + This magic command allows for specifying a target for job submission + to an Azure Quantum workspace, or viewing the list of all available targets. + + The Azure Quantum workspace must previously have been initialized + using the %connect magic command, and the specified target must be + available in the workspace. ".Dedent(), - } - }; - } + Examples = new[] + { + @" + Set the current target for job submission: + ``` + In []: %target TARGET_NAME + Out[]: Active target is now TARGET_NAME + ``` + ".Dedent(), + @" + View the current target and all available targets in the current Azure Quantum workspace: + ``` + In []: %target + Out[]: + ``` + ".Dedent(), + } + }) {} /// /// The object used by this magic command to interact with Azure. @@ -65,18 +60,16 @@ available in the workspace. /// /// Sets or views the target for job submission to the current Azure Quantum workspace. /// - public async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { - channel = channel.WithNewLines(); - Dictionary keyValuePairs = this.ParseInput(input); if (keyValuePairs.Keys.Count > 0) { var targetName = keyValuePairs.Keys.First(); - return await AzureClient.SetActiveTargetAsync(channel, targetName).ToExecutionResult(); + return await AzureClient.SetActiveTargetAsync(channel, targetName); } - return await AzureClient.PrintTargetListAsync(channel).ToExecutionResult(); + return await AzureClient.PrintTargetListAsync(channel); } } } \ No newline at end of file From ccd366f70739b4159933470f94c1ae379cb10cc8 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 8 May 2020 14:53:08 -0400 Subject: [PATCH 04/13] Initial implementation of AzureClient methods --- src/AzureClient/AzureClient.cs | 201 ++++++++++++++++-- src/AzureClient/AzureClient.csproj | 3 + src/AzureClient/Extensions.cs | 56 ++++- src/AzureClient/IAzureClient.cs | 35 +-- src/AzureClient/Magic/AzureClientMagicBase.cs | 10 +- src/AzureClient/Magic/ConnectMagic.cs | 31 +-- src/AzureClient/Magic/StatusMagic.cs | 2 +- src/AzureClient/Magic/SubmitMagic.cs | 2 +- src/AzureClient/Magic/TargetMagic.cs | 7 +- src/Tests/AzureClientMagicTests.cs | 189 ++++++++++++++++ src/Tests/AzureClientTests.cs | 165 +------------- 11 files changed, 483 insertions(+), 218 deletions(-) create mode 100644 src/Tests/AzureClientMagicTests.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 8f2160bae5..639764b426 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -5,30 +5,38 @@ using System; using System.Collections.Generic; -using System.Text; -using Microsoft.Jupyter.Core; +using System.Linq; +using System.IO; using System.Threading.Tasks; - +using Microsoft.Azure.Quantum.Client; +using Microsoft.Azure.Quantum.Storage; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; -using System.Linq; -using System.IO; -using Microsoft.Quantum.Runtime; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.Simulation.Core; namespace Microsoft.Quantum.IQSharp.AzureClient { /// public class AzureClient : IAzureClient { + private string ConnectionString { get; set; } + private string ActiveTargetName { get; set; } + private AuthenticationResult? AuthenticationResult { get; set; } + private IJobsClient? QuantumClient { get; set; } + private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; } + /// /// Creates an AzureClient object. /// public AzureClient() { + ConnectionString = string.Empty; + ActiveTargetName = string.Empty; } /// - public async Task ConnectAsync( + public async Task ConnectAsync( IChannel channel, string subscriptionId, string resourceGroupName, @@ -36,59 +44,208 @@ public async Task ConnectAsync( string storageAccountConnectionString, bool forceLogin = false) { - return AzureClientError.UnknownError; + ConnectionString = storageAccountConnectionString; + + var clientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b"; // Microsoft Quantum Development Kit + var authority = "https://login.microsoftonline.com/common"; + var msalApp = PublicClientApplicationBuilder.Create(clientId).WithAuthority(authority).Build(); + + // Register the token cache for serialization + var cacheFileName = "aad.bin"; + var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE"; + var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName); + if (string.IsNullOrEmpty(cacheDirectory)) + { + cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum"); + } + + var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, clientId).Build(); + var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties); + cacheHelper.RegisterCache(msalApp.UserTokenCache); + + var scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" }; + + bool promptForLogin = forceLogin; + if (!promptForLogin) + { + try + { + var accounts = await msalApp.GetAccountsAsync(); + AuthenticationResult = await msalApp.AcquireTokenSilent( + scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + promptForLogin = true; + } + } + + if (promptForLogin) + { + AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode(scopes, + deviceCodeResult => + { + channel.Stdout(deviceCodeResult.Message); + return Task.FromResult(0); + }).WithAuthority(msalApp.Authority).ExecuteAsync(); + } + + if (AuthenticationResult == null) + { + return AzureClientError.AuthenticationFailed.ToExecutionResult(); + } + + var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken); + + QuantumClient = new JobsClient(); // TODO: pass (credentials) after updating package. + QuantumClient.SubscriptionId = subscriptionId; + QuantumClient.ResourceGroupName = resourceGroupName; + QuantumClient.WorkspaceName = workspaceName; + + ActiveWorkspace = new Azure.Quantum.Workspace( + QuantumClient.SubscriptionId, QuantumClient.ResourceGroupName, + QuantumClient.WorkspaceName, AuthenticationResult?.AccessToken); + + try + { + var jobsList = await QuantumClient.ListJobsAsync(); // QuantumClient.Jobs.ListAsync(); + channel.Stdout($"Found {jobsList.Value.Count()} jobs in Azure Quantum workspace {workspaceName}"); + } + catch (Exception e) + { + channel.Stderr(e.ToString()); + return AzureClientError.WorkspaceNotFound.ToExecutionResult(); + } + + return ActiveWorkspace.ToJupyterTable().ToExecutionResult(); } /// - public async Task PrintConnectionStatusAsync(IChannel channel) + public async Task PrintConnectionStatusAsync(IChannel channel) { - return AzureClientError.UnknownError; + if (ActiveWorkspace == null) + { + return AzureClientError.NotConnected.ToExecutionResult(); + } + + return ActiveWorkspace.ToJupyterTable().ToExecutionResult(); } /// - public async Task SubmitJobAsync( + public async Task SubmitJobAsync( IChannel channel, IOperationResolver operationResolver, string operationName) { - return AzureClientError.UnknownError; + if (QuantumClient == null) + { + channel.Stderr("Must call %connect before submitting a job."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + if (ActiveTargetName == null) + { + channel.Stderr("Must call %target before submitting a job."); + return AzureClientError.NoTarget.ToExecutionResult(); + } + + if (string.IsNullOrEmpty(operationName)) + { + channel.Stderr("Must pass a valid Q# operation name to %submit."); + return AzureClientError.NoOperationName.ToExecutionResult(); + } + + // TODO: Generate the appropriate EntryPointInfo for the given operation. + var operationInfo = operationResolver.Resolve(operationName); + var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); + var entryPointInput = QVoid.Instance; + + // TODO: Create the appropriate QuantumMachine object using ActiveTargetName + var jobStorageHelper = new JobStorageHelper(ConnectionString); + //var machine = new QuantumMachine(ActiveTargetName, Workspace, jobStorageHelper); + //var submissionContext = new QuantumMachine.SubmissionContext(); + //var job = await machine.SubmitAsync(entryPointInfo, entryPointInput, submissionContext); + //return job.Id.ToExecutionResult(); + return operationName.ToExecutionResult(); } /// - public async Task SetActiveTargetAsync( + public async Task SetActiveTargetAsync( IChannel channel, string targetName) { - return AzureClientError.UnknownError; + // TODO: Validate that this target name is valid in the workspace. + ActiveTargetName = targetName; + return $"Active target is now {ActiveTargetName}".ToExecutionResult(); } /// - public async Task PrintActiveTargetAsync( + public async Task PrintActiveTargetAsync( IChannel channel) { - return AzureClientError.UnknownError; + if (string.IsNullOrEmpty(ActiveTargetName)) + { + channel.Stderr("No active target has been set for the current Azure Quantum workspace."); + return AzureClientError.NoTarget.ToExecutionResult(); + } + + return $"Active target is {ActiveTargetName}.".ToExecutionResult(); } /// - public async Task PrintTargetListAsync( + public async Task PrintTargetListAsync( IChannel channel) { - return AzureClientError.UnknownError; + if (ActiveWorkspace == null) + { + channel.Stderr("Must call %connect before listing targets."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + // TODO: Get the list of targets from the updated Azure.Quantum.Client API + //return ActiveWorkspace.Targets.ToJupyterTable().ToExecutionResult(); + return AzureClientError.NoTarget.ToExecutionResult(); } /// - public async Task PrintJobStatusAsync( + public async Task PrintJobStatusAsync( IChannel channel, string jobId) { - return AzureClientError.UnknownError; + if (QuantumClient == null) + { + channel.Stderr("Must call %connect before getting job status."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + var jobDetails = await QuantumClient.GetJobAsync(jobId); // QuantumClient.Jobs.GetAsync(jobId); + if (jobDetails == null) + { + channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + return jobDetails.ToJupyterTable().ToExecutionResult(); } /// - public async Task PrintJobListAsync( + public async Task PrintJobListAsync( IChannel channel) { - return AzureClientError.UnknownError; + if (QuantumClient == null) + { + channel.Stderr("Must call %connect before listing jobs."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + var jobsList = await QuantumClient.ListJobsAsync(); // QuantumClient.Jobs.ListAsync(); + if (jobsList.Value == null || jobsList.Value.Count() == 0) + { + channel.Stderr("No jobs found in current Azure Quantum workspace."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + return jobsList.Value.ToJupyterTable().ToExecutionResult(); } } } diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index f518fde626..4173885635 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,8 +13,11 @@ + + + diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 478121cece..494a8a3f82 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -5,8 +5,9 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; @@ -34,7 +35,7 @@ public static void AddAzureClient(this IServiceCollection services) public static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => new ExecutionResult { - Status = azureClientError == AzureClientError.Success ? ExecuteStatus.Ok : ExecuteStatus.Error, + Status = ExecuteStatus.Error, Output = azureClientError }; @@ -46,5 +47,56 @@ public static ExecutionResult ToExecutionResult(this AzureClientError azureClien /// public static async Task ToExecutionResult(this Task task) => (await task).ToExecutionResult(); + + internal static Table ToJupyterTable(this JobDetails jobDetails) => + new List { jobDetails }.ToJupyterTable(); + + internal static Table ToJupyterTable(this IEnumerable jobsList) => + new Table + { + Columns = new List<(string, Func)> + { + ("Id", jobDetails => jobDetails.Id), + ("ProviderId", jobDetails => jobDetails.ProviderId), + ("Status", jobDetails => jobDetails.Status) + }, + Rows = jobsList.ToList() + }; + + internal static Table ToJupyterTable(this Azure.Quantum.IWorkspace workspace) => + new List { workspace }.ToJupyterTable(); + + internal static Table ToJupyterTable(this IEnumerable workspacesList) => + new Table + { + Columns = new List<(string, Func)> + { + // TODO: Uncomment this after updating the Azure.Quantum.Client API + //("Name", workspace => workspace.Name), + //("Type", workspace => workspace.Type), + //("Location", workspace => workspace.Location), + ("TEMP_AsString", workspace => workspace.ToString()), + }, + Rows = workspacesList.ToList() + }; + + // TODO: Implement this for providers and targets once they are exposed + // through the Azure.Quantum.Client API. + + //internal static Table ToJupyterTable(this Provider provider) => + // new List { provider }.ToJupyterTable(); + + //internal static Table ToJupyterTable(this IEnumerable providersList) => + // new Table + // { + // Columns = new List<(string, Func)> + // { + // ("Name", provider => provider.ApplicationName), + // ("ProviderId", provider => provider.ProviderId), + // ("ProviderSku", provider => provider.ProviderSku), + // ("ProvisioningState", provider => provider.ProvisioningState) + // }, + // Rows = providersList.ToList() + // }; } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 84ee4b3fd0..efc9de3166 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -16,20 +16,15 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public enum AzureClientError { - /// - /// Method completed successfully. - /// - Success = 0, - /// /// Method completed with an unknown error. /// - UnknownError = 9999, + UnknownError = 0, /// /// No connection has been made to any Azure Quantum workspace. /// - NoWorkspace = 1, + NotConnected = 1, /// /// A target has not yet been configured for job submission. @@ -45,6 +40,16 @@ public enum AzureClientError /// No Q# operation name was provided where one was required. /// NoOperationName = 4, + + /// + /// Authentication with the Azure service failed. + /// + AuthenticationFailed = 5, + + /// + /// A workspace meeting the specified criteria was not found. + /// + WorkspaceNotFound = 6, } /// @@ -56,7 +61,7 @@ public interface IAzureClient /// /// Connects to the specified Azure Quantum workspace, first logging into Azure if necessary. /// - public Task ConnectAsync( + public Task ConnectAsync( IChannel channel, string subscriptionId, string resourceGroupName, @@ -67,13 +72,13 @@ public Task ConnectAsync( /// /// Prints a string describing the current connection status. /// - public Task PrintConnectionStatusAsync( + public Task PrintConnectionStatusAsync( IChannel channel); /// /// Submits the specified Q# operation as a job to the currently active target. /// - public Task SubmitJobAsync( + public Task SubmitJobAsync( IChannel channel, IOperationResolver operationResolver, string operationName); @@ -81,33 +86,33 @@ public Task SubmitJobAsync( /// /// Sets the specified target for job submission. /// - public Task SetActiveTargetAsync( + public Task SetActiveTargetAsync( IChannel channel, string targetName); /// /// Prints the specified target for job submission. /// - public Task PrintActiveTargetAsync( + public Task PrintActiveTargetAsync( IChannel channel); /// /// Prints the list of targets currently provisioned in the current workspace. /// - public Task PrintTargetListAsync( + public Task PrintTargetListAsync( IChannel channel); /// /// Prints the job status corresponding to the given job ID. /// - public Task PrintJobStatusAsync( + public Task PrintJobStatusAsync( IChannel channel, string jobId); /// /// Prints a list of all jobs in the current workspace. /// - public Task PrintJobListAsync( + public Task PrintJobListAsync( IChannel channel); } } diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs index 06dd581aca..86450104c9 100644 --- a/src/AzureClient/Magic/AzureClientMagicBase.cs +++ b/src/AzureClient/Magic/AzureClientMagicBase.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using Microsoft.Jupyter.Core; @@ -36,14 +38,14 @@ public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentat /// and translates the result from AzureClient.AzureClientError /// to Jupyter.Core.ExecutionResult. /// - public Func> SafeExecute(Func> azureClientMagic) => + public Func> SafeExecute(Func> azureClientMagic) => async (input, channel) => { channel = channel.WithNewLines(); try { - return await azureClientMagic(input, channel).ToExecutionResult(); + return await azureClientMagic(input, channel); } catch (InvalidWorkspaceException ws) { @@ -75,13 +77,13 @@ public Dictionary ParseInput(string input) var key = tokens[0].Trim(); var value = (tokens.Length == 1) ? string.Empty : tokens[1].Trim(); keyValuePairs[key] = value; - } + } return keyValuePairs; } /// /// Executes the magic command functionality for the given input. /// - public abstract Task RunAsync(string input, IChannel channel); + public abstract Task RunAsync(string input, IChannel channel); } } diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index bff4d75e86..10a570f4a6 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -16,6 +16,13 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class ConnectMagic : AzureClientMagicBase { + private const string + ParamName_Login = "login", + ParamName_StorageAccountConnectionString = "storageAccountConnectionString", + ParamName_SubscriptionId = "subscriptionId", + ParamName_ResourceGroupName = "resourceGroupName", + ParamName_WorkspaceName = "workspaceName"; + /// /// Constructs a new magic command given an IAzureClient object. /// @@ -40,23 +47,23 @@ as specified by a valid connection string OR a valid combination of ``` ".Dedent(), - @" + $@" Connect to an Azure Quantum workspace using a connection string: ``` - In []: %connect connectionString=CONNECTION_STRING + In []: %connect {ParamName_StorageAccountConnectionString}=CONNECTION_STRING Out[]: Connected to WORKSPACE_NAME ``` ".Dedent(), - @" + $@" Connect to an Azure Quantum workspace and force a credential prompt: ``` - In []: %connect login connectionString=CONNECTION_STRING + In []: %connect {ParamName_Login} {ParamName_StorageAccountConnectionString}=CONNECTION_STRING 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 WORKSPACE_NAME ``` - Use the `login` option if you want to bypass any saved or cached + Use the `{ParamName_Login}` option if you want to bypass any saved or cached credentials when connecting to Azure. ".Dedent() } @@ -66,23 +73,23 @@ credentials when connecting to Azure. /// Connects to an Azure workspace given a subscription ID, resource group name, /// workspace name, and connection string as a JSON-encoded object. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = this.ParseInput(input); + Dictionary keyValuePairs = ParseInput(input); string storageAccountConnectionString; - keyValuePairs.TryGetValue("storageAccountConnectionString", out storageAccountConnectionString); + keyValuePairs.TryGetValue(ParamName_StorageAccountConnectionString, out storageAccountConnectionString); if (string.IsNullOrEmpty(storageAccountConnectionString)) { return await AzureClient.PrintConnectionStatusAsync(channel); } string subscriptionId, resourceGroupName, workspaceName; - keyValuePairs.TryGetValue("subscriptionId", out subscriptionId); - keyValuePairs.TryGetValue("resourceGroupName", out resourceGroupName); - keyValuePairs.TryGetValue("workspaceName", out workspaceName); + keyValuePairs.TryGetValue(ParamName_SubscriptionId, out subscriptionId); + keyValuePairs.TryGetValue(ParamName_ResourceGroupName, out resourceGroupName); + keyValuePairs.TryGetValue(ParamName_WorkspaceName, out workspaceName); - bool forceLogin = keyValuePairs.ContainsKey("login"); + bool forceLogin = keyValuePairs.ContainsKey(ParamName_Login); return await AzureClient.ConnectAsync( channel, subscriptionId, diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index 474eac4a2c..8048e40bc6 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -55,7 +55,7 @@ in the current workspace will be displayed. /// Displays the status corresponding to a given job ID, if provided, /// or all jobs in the active workspace. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { Dictionary keyValuePairs = this.ParseInput(input); if (keyValuePairs.Keys.Count > 0) diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 59c9e7994f..fe9b30523c 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -56,7 +56,7 @@ The Azure Quantum workspace must previously have been initialized /// Submits a new job to an Azure Quantum workspace given a Q# operation /// name that is present in the current Q# Jupyter workspace. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { Dictionary keyValuePairs = this.ParseInput(input); var operationName = keyValuePairs.Keys.FirstOrDefault(); diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 8a47b0e9ae..01ffa4def0 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -52,15 +52,10 @@ available in the workspace. } }) {} - /// - /// The object used by this magic command to interact with Azure. - /// - public IAzureClient AzureClient { get; } - /// /// Sets or views the target for job submission to the current Azure Quantum workspace. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel) { Dictionary keyValuePairs = this.ParseInput(input); if (keyValuePairs.Keys.Count > 0) diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs new file mode 100644 index 0000000000..c7846ada42 --- /dev/null +++ b/src/Tests/AzureClientMagicTests.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; + +namespace Tests.IQSharp +{ + public static class AzureClientMagicTestExtensions + { + public static void Test(this MagicSymbol magic, string input, ExecuteStatus expected = ExecuteStatus.Ok) + { + var result = magic.Execute(input, new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == expected); + } + } + + [TestClass] + public class AzureClientMagicTests + { + private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; + private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; + private readonly string workspaceName = "TEST_WORKSPACE_NAME"; + private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; + private readonly string jobId = "TEST_JOB_ID"; + private readonly string operationName = "TEST_OPERATION_NAME"; + private readonly string targetName = "TEST_TARGET_NAME"; + + [TestMethod] + public void TestConnectMagic() + { + var azureClient = new MockAzureClient(); + var connectMagic = new ConnectMagic(azureClient); + + // unrecognized input + connectMagic.Test($"invalid"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintConnectionStatus); + + // valid input + connectMagic.Test( + @$"subscriptionId={subscriptionId} + resourceGroupName={resourceGroupName} + workspaceName={workspaceName} + storageAccountConnectionString={storageAccountConnectionString}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.Connect); + Assert.IsFalse(azureClient.ForceLogin); + Assert.AreEqual(azureClient.ConnectionString, storageAccountConnectionString); + + // valid input with forced login + connectMagic.Test( + @$"login + subscriptionId={subscriptionId} + resourceGroupName={resourceGroupName} + workspaceName={workspaceName} + storageAccountConnectionString={storageAccountConnectionString}"); + + Assert.IsTrue(azureClient.ForceLogin); + } + + [TestMethod] + public void TestStatusMagic() + { + // no arguments - should print job list + var azureClient = new MockAzureClient(); + var statusMagic = new StatusMagic(azureClient); + statusMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobList); + + // single argument - should print job status + azureClient = new MockAzureClient(); + statusMagic = new StatusMagic(azureClient); + statusMagic.Test($"{jobId}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobStatus); + } + + [TestMethod] + public void TestSubmitMagic() + { + // no arguments + var azureClient = new MockAzureClient(); + var operationResolver = new MockOperationResolver(); + var submitMagic = new SubmitMagic(operationResolver, azureClient); + submitMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + + // single argument + submitMagic.Test($"{operationName}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + Assert.IsTrue(azureClient.SubmittedJobs.Contains(operationName)); + } + + [TestMethod] + public void TestTargetMagic() + { + // no arguments - should print target list + var azureClient = new MockAzureClient(); + var targetMagic = new TargetMagic(azureClient); + targetMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintTargetList); + + // single argument - should set active target + azureClient = new MockAzureClient(); + targetMagic = new TargetMagic(azureClient); + targetMagic.Test(targetName); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); + } + } + + internal enum AzureClientAction + { + None, + Connect, + SetActiveTarget, + SubmitJob, + PrintActiveTarget, + PrintConnectionStatus, + PrintJobList, + PrintJobStatus, + PrintTargetList, + } + + public class MockAzureClient : IAzureClient + { + internal AzureClientAction LastAction = AzureClientAction.None; + internal string ConnectionString = string.Empty; + internal bool ForceLogin = false; + internal string ActiveTargetName = string.Empty; + internal List SubmittedJobs = new List(); + + public async Task SetActiveTargetAsync(IChannel channel, string targetName) + { + LastAction = AzureClientAction.SetActiveTarget; + ActiveTargetName = targetName; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + { + LastAction = AzureClientAction.SubmitJob; + SubmittedJobs.Add(operationName); + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool forceLogin) + { + LastAction = AzureClientAction.Connect; + ConnectionString = storageAccountConnectionString; + ForceLogin = forceLogin; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintActiveTargetAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintActiveTarget; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintConnectionStatusAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintConnectionStatus; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintJobListAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintJobList; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintJobStatusAsync(IChannel channel, string jobId) + { + LastAction = AzureClientAction.PrintJobStatus; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintTargetListAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintTargetList; + return ExecuteStatus.Ok.ToExecutionResult(); + } + } +} diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index a286496270..d6668db4f5 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -4,22 +4,14 @@ #nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.Quantum.IQSharp.AzureClient; using System.Linq; -using System.Collections.Generic; using Microsoft.Jupyter.Core; -using System.Threading.Tasks; -using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; namespace Tests.IQSharp { public static class AzureClientTestExtensions { - public static void Test(this MagicSymbol magic, string input, ExecuteStatus expected = ExecuteStatus.Ok) - { - var result = magic.Execute(input, new MockChannel()).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == expected); - } } [TestClass] @@ -34,156 +26,19 @@ public class AzureClientTests private readonly string targetName = "TEST_TARGET_NAME"; [TestMethod] - public void TestConnectMagic() - { - var azureClient = new MockAzureClient(); - var connectMagic = new ConnectMagic(azureClient); - - // unrecognized input - connectMagic.Test($"invalid"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintConnectionStatus); - - // valid input - connectMagic.Test( - @$"subscriptionId={subscriptionId} - resourceGroupName={resourceGroupName} - workspaceName={workspaceName} - storageAccountConnectionString={storageAccountConnectionString}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.Connect); - Assert.IsFalse(azureClient.ForceLogin); - Assert.AreEqual(azureClient.ConnectionString, storageAccountConnectionString); - - // valid input with forced login - connectMagic.Test( - @$"login - subscriptionId={subscriptionId} - resourceGroupName={resourceGroupName} - workspaceName={workspaceName} - storageAccountConnectionString={storageAccountConnectionString}"); - - Assert.IsTrue(azureClient.ForceLogin); - } - - [TestMethod] - public void TestStatusMagic() - { - // no arguments - should print job list - var azureClient = new MockAzureClient(); - var statusMagic = new StatusMagic(azureClient); - statusMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobList); - - // single argument - should print job status - azureClient = new MockAzureClient(); - statusMagic = new StatusMagic(azureClient); - statusMagic.Test($"{jobId}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobStatus); - } - - [TestMethod] - public void TestSubmitMagic() - { - // no arguments - var azureClient = new MockAzureClient(); - var operationResolver = new MockOperationResolver(); - var submitMagic = new SubmitMagic(operationResolver, azureClient); - submitMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); - - // single argument - submitMagic.Test($"{operationName}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); - Assert.IsTrue(azureClient.SubmittedJobs.Contains(operationName)); - } - - [TestMethod] - public void TestTargetMagic() - { - // no arguments - should print target list - var azureClient = new MockAzureClient(); - var targetMagic = new TargetMagic(azureClient); - targetMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintTargetList); - - // single argument - should set active target - azureClient = new MockAzureClient(); - targetMagic = new TargetMagic(azureClient); - targetMagic.Test(targetName); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); - } - } - - internal enum AzureClientAction - { - None, - Connect, - SetActiveTarget, - SubmitJob, - PrintActiveTarget, - PrintConnectionStatus, - PrintJobList, - PrintJobStatus, - PrintTargetList, - } - - public class MockAzureClient : IAzureClient - { - internal AzureClientAction LastAction = AzureClientAction.None; - internal string ConnectionString = string.Empty; - internal bool ForceLogin = false; - internal string ActiveTargetName = string.Empty; - internal List SubmittedJobs = new List(); - - public async Task SetActiveTargetAsync(IChannel channel, string targetName) + public void TestTargets() { - LastAction = AzureClientAction.SetActiveTarget; - ActiveTargetName = targetName; - return AzureClientError.Success; - } + var azureClient = new AzureClient(); - public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) - { - LastAction = AzureClientAction.SubmitJob; - SubmittedJobs.Add(operationName); - return AzureClientError.Success; - } + var result = azureClient.SetActiveTargetAsync(new MockChannel(), targetName).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Ok); - public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool forceLogin) - { - LastAction = AzureClientAction.Connect; - ConnectionString = storageAccountConnectionString; - ForceLogin = forceLogin; - return AzureClientError.Success; - } + result = azureClient.PrintActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Ok); + Assert.IsNotNull(result.Output); - public async Task PrintActiveTargetAsync(IChannel channel) - { - LastAction = AzureClientAction.PrintActiveTarget; - return AzureClientError.Success; - } - - public async Task PrintConnectionStatusAsync(IChannel channel) - { - LastAction = AzureClientAction.PrintConnectionStatus; - return AzureClientError.Success; - } - - public async Task PrintJobListAsync(IChannel channel) - { - LastAction = AzureClientAction.PrintJobList; - return AzureClientError.Success; - } - - public async Task PrintJobStatusAsync(IChannel channel, string jobId) - { - LastAction = AzureClientAction.PrintJobStatus; - return AzureClientError.Success; - } - - public async Task PrintTargetListAsync(IChannel channel) - { - LastAction = AzureClientAction.PrintTargetList; - return AzureClientError.Success; + result = azureClient.PrintTargetListAsync(new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Error); } } } From 2ea0041bba794ee1d73f52266cd4af967bb2e45e Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 11 May 2020 11:12:11 -0400 Subject: [PATCH 05/13] Move AbstractMagic to Jupyter project --- src/{Kernel => Jupyter}/Magic/AbstractMagic.cs | 3 +-- src/Kernel/Magic/LsMagicMagic.cs | 2 +- src/Kernel/Magic/WhoMagic.cs | 2 +- src/Kernel/Magic/WorkspaceMagic.cs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) rename src/{Kernel => Jupyter}/Magic/AbstractMagic.cs (97%) diff --git a/src/Kernel/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs similarity index 97% rename from src/Kernel/Magic/AbstractMagic.cs rename to src/Jupyter/Magic/AbstractMagic.cs index 9babaf2ca1..2823f78b24 100644 --- a/src/Kernel/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -6,9 +6,8 @@ using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; -using Microsoft.Quantum.IQSharp.Kernel; -namespace Microsoft.Quantum.IQSharp.Kernel +namespace Microsoft.Quantum.IQSharp.Jupyter { /// /// Abstract base class for IQ# magic symbols. diff --git a/src/Kernel/Magic/LsMagicMagic.cs b/src/Kernel/Magic/LsMagicMagic.cs index 891d2f92b6..05e1037e00 100644 --- a/src/Kernel/Magic/LsMagicMagic.cs +++ b/src/Kernel/Magic/LsMagicMagic.cs @@ -5,7 +5,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.Kernel { diff --git a/src/Kernel/Magic/WhoMagic.cs b/src/Kernel/Magic/WhoMagic.cs index 7efadeddb5..9a8c514f4d 100644 --- a/src/Kernel/Magic/WhoMagic.cs +++ b/src/Kernel/Magic/WhoMagic.cs @@ -5,7 +5,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.Kernel { diff --git a/src/Kernel/Magic/WorkspaceMagic.cs b/src/Kernel/Magic/WorkspaceMagic.cs index 0f347295d3..7ef9ee2996 100644 --- a/src/Kernel/Magic/WorkspaceMagic.cs +++ b/src/Kernel/Magic/WorkspaceMagic.cs @@ -5,7 +5,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.Kernel { From 6032442923e2fa3604efbf983aaf11b2a3f7b21f Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 11 May 2020 13:21:44 -0400 Subject: [PATCH 06/13] Add key=value input parameter parsing to AbstractMagic --- src/Jupyter/Magic/AbstractMagic.cs | 57 +++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 2823f78b24..534c8a2438 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; +using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.Jupyter { @@ -14,13 +16,16 @@ namespace Microsoft.Quantum.IQSharp.Jupyter /// public abstract class AbstractMagic : MagicSymbol { + private string FirstArgumentName; + /// /// Constructs a new magic symbol given its name and documentation. /// - public AbstractMagic(string keyword, Documentation docs) + public AbstractMagic(string keyword, Documentation docs, string firstArgumentName = "") { this.Name = $"%{keyword}"; this.Documentation = docs; + this.FirstArgumentName = firstArgumentName; this.Kind = SymbolKind.Magic; this.Execute = SafeExecute(this.Run); @@ -77,6 +82,56 @@ public static (string, Dictionary) ParseInput(string input) return (name, args); } + /// + /// Parses the input to a magic command, interpreting the input as + /// a name followed by a JSON-serialized dictionary. + /// + public static Dictionary JsonToDict(string input) => + !string.IsNullOrEmpty(input) ? JsonConverters.JsonToDict(input) : new Dictionary { }; + + /// + /// Parses the input parameters for a given magic symbol and returns a + /// Dictionary with the names and values of the parameters. + /// + public Dictionary ParseInputParameters(string input) + { + Dictionary keyValuePairs = new Dictionary(); + + var args = input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries); + if (args.Length > 0 && + !args[0].StartsWith("{") && + !args[0].Contains("=") && + !string.IsNullOrEmpty(FirstArgumentName)) + { + keyValuePairs[FirstArgumentName] = args[0]; + args = args.Where((_, index) => index != 0).ToArray(); + } + + // See if the remaining arguments look like JSON. If so, try to parse as JSON. + // Otherwise, try to parse as key=value pairs. + if (args.Length > 0 && args[0].StartsWith("{")) + { + var jsonArgs = JsonToDict(string.Join(" ", args)); + foreach (var (key, jsonValue) in jsonArgs) + { + keyValuePairs[key] = jsonValue; + } + } + else + { + foreach (string arg in args) + { + var tokens = arg.Split("=", 2); + var key = tokens[0].Trim(); + object value = (tokens.Length == 1) ? true as object : tokens[1].Trim() as object; + var jsonValue = JObject.FromObject(value).ToString(Newtonsoft.Json.Formatting.None); + keyValuePairs[key] = jsonValue; + } + } + + return keyValuePairs; + } + /// /// A method to be run when the magic command is executed. /// From b5fd23299081fe361a2ed81e64640c984af952d0 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 13 May 2020 08:52:33 -0400 Subject: [PATCH 07/13] Initial qsharp.azure Python implementation --- src/Python/qsharp/azure.py | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/Python/qsharp/azure.py diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py new file mode 100644 index 0000000000..3cec5bcb81 --- /dev/null +++ b/src/Python/qsharp/azure.py @@ -0,0 +1,55 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +## +# azure.py: enables using Q# quantum execution on Azure Quantum from Python. +## +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +## + +## IMPORTS ## + +import qsharp +import json +import typing +from typing import List, Dict, Callable, Any + +from qsharp.serialization import map_tuples +from typing import List, Tuple, Dict, Iterable +from enum import Enum + +## LOGGING ## + +import logging +logger = logging.getLogger(__name__) + +## EXPORTS ## + +__all__ = [ + 'connect', + 'target', + 'submit', + 'execute', + 'status', + 'output' +] + +## FUNCTIONS ## + +def connect(**params) -> Any: + return qsharp.client._execute_magic(f"connect", raise_on_stderr=False, **params) + +def target(name : str = '', **params) -> Any: + return qsharp.client._execute_magic(f"target {name}", raise_on_stderr=False, **params) + +def submit(op, **params) -> Any: + return qsharp.client._execute_callable_magic("submit", op, raise_on_stderr=False, **params) + +def execute(op, **params) -> Any: + return qsharp.client._execute_callable_magic("execute", op, raise_on_stderr=False, **params) + +def status(jobId : str = '', **params) -> Any: + return qsharp.client._execute_magic(f"status {jobId}", raise_on_stderr=False, **params) + +def output(jobId : str = '', **params) -> Any: + return qsharp.client._execute_magic(f"output {jobId}", raise_on_stderr=False, **params) From 134eb701628ddeb1e4f6cca6fd9669c12f12cb53 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 13 May 2020 08:53:03 -0400 Subject: [PATCH 08/13] Begin to unify magic command parsing --- src/AzureClient/AzureClient.csproj | 4 +- src/AzureClient/Extensions.cs | 22 ++++++- src/AzureClient/IAzureClient.cs | 8 +++ src/AzureClient/Magic/AzureClientMagicBase.cs | 61 +++---------------- src/AzureClient/Magic/ConnectMagic.cs | 16 +++-- src/AzureClient/Magic/StatusMagic.cs | 2 +- src/AzureClient/Magic/SubmitMagic.cs | 2 +- src/AzureClient/Magic/TargetMagic.cs | 14 +++-- src/AzureClient/Resources.cs | 33 ++++++++++ src/Jupyter/Extensions.cs | 13 ++++ src/Jupyter/Magic/AbstractMagic.cs | 40 +++++++----- src/Kernel/Magic/Simulate.cs | 8 ++- 12 files changed, 133 insertions(+), 90 deletions(-) create mode 100644 src/AzureClient/Resources.cs diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index 4173885635..32bae3bed8 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -13,7 +13,7 @@ - + diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 494a8a3f82..c0245d16c0 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -5,11 +5,14 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.QsCompiler.Serialization; +using Newtonsoft.Json; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -36,9 +39,24 @@ public static ExecutionResult ToExecutionResult(this AzureClientError azureClien new ExecutionResult { Status = ExecuteStatus.Error, - Output = azureClientError + Output = azureClientError.ToDescription() }; + /// + /// Returns the string value of the DescriptionAttribute for the given + /// AzureClientError enumeration value. + /// + /// + /// + public static string ToDescription(this AzureClientError azureClientError) + { + var attributes = azureClientError + .GetType() + .GetField(azureClientError.ToString()) + .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; + return attributes?.Length > 0 ? attributes[0].Description : string.Empty; + } + /// /// Encapsulates a given AzureClientError as the result of an execution. /// @@ -47,7 +65,7 @@ public static ExecutionResult ToExecutionResult(this AzureClientError azureClien /// public static async Task ToExecutionResult(this Task task) => (await task).ToExecutionResult(); - + internal static Table ToJupyterTable(this JobDetails jobDetails) => new List { jobDetails }.ToJupyterTable(); diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index efc9de3166..2e896201bf 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -8,6 +8,7 @@ using System.Text; using Microsoft.Jupyter.Core; using System.Threading.Tasks; +using System.ComponentModel; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -19,36 +20,43 @@ public enum AzureClientError /// /// Method completed with an unknown error. /// + [Description(Resources.AzureClientError_UnknownError)] UnknownError = 0, /// /// No connection has been made to any Azure Quantum workspace. /// + [Description(Resources.AzureClientError_NotConnected)] NotConnected = 1, /// /// A target has not yet been configured for job submission. /// + [Description(Resources.AzureClientError_NoTarget)] NoTarget = 2, /// /// A job meeting the specified criteria was not found. /// + [Description(Resources.AzureClientError_JobNotFound)] JobNotFound = 3, /// /// No Q# operation name was provided where one was required. /// + [Description(Resources.AzureClientError_NoOperationName)] NoOperationName = 4, /// /// Authentication with the Azure service failed. /// + [Description(Resources.AzureClientError_AuthenticationFailed)] AuthenticationFailed = 5, /// /// A workspace meeting the specified criteria was not found. /// + [Description(Resources.AzureClientError_WorkspaceNotFound)] WorkspaceNotFound = 6, } diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs index 86450104c9..9a2071ea0b 100644 --- a/src/AzureClient/Magic/AzureClientMagicBase.cs +++ b/src/AzureClient/Magic/AzureClientMagicBase.cs @@ -6,13 +6,14 @@ using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.AzureClient { /// /// Base class used for Azure Client magic commands. /// - public abstract class AzureClientMagicBase : MagicSymbol + public abstract class AzureClientMagicBase : AbstractMagic { /// /// The object used by this magic command to interact with Azure. @@ -23,63 +24,15 @@ public abstract class AzureClientMagicBase : MagicSymbol /// Constructs the Azure Client magic command with the specified keyword /// and documentation. /// - public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentation docs) + public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentation docs): + base(keyword, docs) { this.AzureClient = azureClient; - this.Name = $"%{keyword}"; - this.Documentation = docs; - - this.Kind = SymbolKind.Magic; - this.Execute = SafeExecute(this.RunAsync); } - /// - /// Performs Azure Client magic commands with safe exception handling - /// and translates the result from AzureClient.AzureClientError - /// to Jupyter.Core.ExecutionResult. - /// - public Func> SafeExecute(Func> azureClientMagic) => - async (input, channel) => - { - channel = channel.WithNewLines(); - - try - { - return await azureClientMagic(input, channel); - } - catch (InvalidWorkspaceException ws) - { - foreach (var m in ws.Errors) channel.Stderr(m); - return ExecuteStatus.Error.ToExecutionResult(); - } - catch (AggregateException agg) - { - foreach (var e in agg.InnerExceptions) channel.Stderr(e?.Message); - return ExecuteStatus.Error.ToExecutionResult(); - } - catch (Exception e) - { - channel.Stderr(e.Message); - return ExecuteStatus.Error.ToExecutionResult(); - } - }; - - /// - /// Parses the input parameters for a given magic symbol and returns a - /// Dictionary with the names and values of the parameters. - /// - public Dictionary ParseInput(string input) - { - Dictionary keyValuePairs = new Dictionary(); - foreach (string arg in input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries)) - { - var tokens = arg.Split("=", 2); - var key = tokens[0].Trim(); - var value = (tokens.Length == 1) ? string.Empty : tokens[1].Trim(); - keyValuePairs[key] = value; - } - return keyValuePairs; - } + /// + public override ExecutionResult Run(string input, IChannel channel) => + RunAsync(input, channel).GetAwaiter().GetResult(); /// /// Executes the magic command functionality for the given input. diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 10a570f4a6..82cd888601 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -75,21 +76,18 @@ credentials when connecting to Azure. /// public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = ParseInput(input); + var inputParameters = ParseInputParameters(input); - string storageAccountConnectionString; - keyValuePairs.TryGetValue(ParamName_StorageAccountConnectionString, out storageAccountConnectionString); + var storageAccountConnectionString = inputParameters.DecodeParameter(ParamName_StorageAccountConnectionString); if (string.IsNullOrEmpty(storageAccountConnectionString)) { return await AzureClient.PrintConnectionStatusAsync(channel); } - string subscriptionId, resourceGroupName, workspaceName; - keyValuePairs.TryGetValue(ParamName_SubscriptionId, out subscriptionId); - keyValuePairs.TryGetValue(ParamName_ResourceGroupName, out resourceGroupName); - keyValuePairs.TryGetValue(ParamName_WorkspaceName, out workspaceName); - - bool forceLogin = keyValuePairs.ContainsKey(ParamName_Login); + var subscriptionId = inputParameters.DecodeParameter(ParamName_SubscriptionId); + var resourceGroupName = inputParameters.DecodeParameter(ParamName_ResourceGroupName); + var workspaceName = inputParameters.DecodeParameter(ParamName_WorkspaceName); + var forceLogin = inputParameters.DecodeParameter(ParamName_Login); return await AzureClient.ConnectAsync( channel, subscriptionId, diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index 8048e40bc6..3dbe9932a6 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -57,7 +57,7 @@ in the current workspace will be displayed. /// public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = this.ParseInput(input); + Dictionary keyValuePairs = ParseInputParameters(input); if (keyValuePairs.Keys.Count > 0) { var jobId = keyValuePairs.Keys.First(); diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index fe9b30523c..d89da233b3 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -58,7 +58,7 @@ The Azure Quantum workspace must previously have been initialized /// public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = this.ParseInput(input); + Dictionary keyValuePairs = ParseInputParameters(input); var operationName = keyValuePairs.Keys.FirstOrDefault(); return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); } diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 01ffa4def0..5e63c5b167 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -16,6 +17,9 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class TargetMagic : AzureClientMagicBase { + private const string + ParamName_TargetName = "name"; + /// /// Constructs a new magic command given an IAzureClient object. /// @@ -50,17 +54,19 @@ available in the workspace. ``` ".Dedent(), } - }) {} + }) + { + } /// /// Sets or views the target for job submission to the current Azure Quantum workspace. /// public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = this.ParseInput(input); - if (keyValuePairs.Keys.Count > 0) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_TargetName); + if (inputParameters.Count() > 0) { - var targetName = keyValuePairs.Keys.First(); + var targetName = inputParameters.DecodeParameter(ParamName_TargetName); return await AzureClient.SetActiveTargetAsync(channel, targetName); } diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs new file mode 100644 index 0000000000..829ae77acc --- /dev/null +++ b/src/AzureClient/Resources.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// This class contains resources that will eventually be exposed to localization. + /// + internal static class Resources + { + public const string AzureClientError_UnknownError = + "An unknown error occurred."; + + public const string AzureClientError_NotConnected = + "Not connected to any Azure Quantum workspace."; + + public const string AzureClientError_NoTarget = + "No execution target has been configured for Azure Quantum job submission."; + + public const string AzureClientError_JobNotFound = + "No job with the given ID was found in the current Azure Quantum workspace."; + + public const string AzureClientError_NoOperationName = + "No Q# operation name was specified for Azure Quantum job submission."; + + public const string AzureClientError_AuthenticationFailed = + "Failed to authenticate to the specified Azure Quantum workspace."; + + public const string AzureClientError_WorkspaceNotFound = + "No Azure Quantum workspace was found that matches the specified criteria."; + } +} diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs index 5a602d89d4..b0b4a6b22f 100644 --- a/src/Jupyter/Extensions.cs +++ b/src/Jupyter/Extensions.cs @@ -9,6 +9,7 @@ using Microsoft.Quantum.Simulation.Common; using Microsoft.Quantum.Simulation.Core; using Microsoft.Quantum.Simulation.Simulators; +using Newtonsoft.Json; namespace Microsoft.Quantum.IQSharp.Jupyter { @@ -174,5 +175,17 @@ public static string Dedent(this string text) var leftTrimRegex = new Regex(@$"^[ \t]{{{minWhitespace}}}", RegexOptions.Multiline); return leftTrimRegex.Replace(text, ""); } + + /// + /// Retrieves and JSON-decodes the value for the given parameter name. + /// + public static T DecodeParameter(this Dictionary parameters, string parameterName) + { + if (!parameters.TryGetValue(parameterName, out string parameterValue)) + { + return default(T); + } + return (T)(JsonConvert.DeserializeObject(parameterValue) ?? default(T)); + } } } diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 534c8a2438..1b0cd24c3a 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.QsCompiler.Serialization; using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.Jupyter @@ -16,16 +18,13 @@ namespace Microsoft.Quantum.IQSharp.Jupyter /// public abstract class AbstractMagic : MagicSymbol { - private string FirstArgumentName; - /// /// Constructs a new magic symbol given its name and documentation. /// - public AbstractMagic(string keyword, Documentation docs, string firstArgumentName = "") + public AbstractMagic(string keyword, Documentation docs) { this.Name = $"%{keyword}"; this.Documentation = docs; - this.FirstArgumentName = firstArgumentName; this.Kind = SymbolKind.Magic; this.Execute = SafeExecute(this.Run); @@ -91,30 +90,38 @@ public static Dictionary JsonToDict(string input) => /// /// Parses the input parameters for a given magic symbol and returns a - /// Dictionary with the names and values of the parameters. + /// Dictionary with the names and values of the parameters, + /// where the values of the Dictionary are JSON-serialized objects. /// - public Dictionary ParseInputParameters(string input) + public Dictionary ParseInputParameters(string input, string firstParameterInferredName = "") { - Dictionary keyValuePairs = new Dictionary(); + Dictionary inputParameters = new Dictionary(); var args = input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries); + + // 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.Length > 0 && !args[0].StartsWith("{") && !args[0].Contains("=") && - !string.IsNullOrEmpty(FirstArgumentName)) + !string.IsNullOrEmpty(firstParameterInferredName)) { - keyValuePairs[FirstArgumentName] = args[0]; + using (var writer = new StringWriter()) + { + Json.Serializer.Serialize(writer, args[0]); + inputParameters[firstParameterInferredName] = writer.ToString(); + } args = args.Where((_, index) => index != 0).ToArray(); } // See if the remaining arguments look like JSON. If so, try to parse as JSON. - // Otherwise, try to parse as key=value pairs. + // Otherwise, try to parse as key=value pairs and serialize into the dictionary as JSON. if (args.Length > 0 && args[0].StartsWith("{")) { var jsonArgs = JsonToDict(string.Join(" ", args)); foreach (var (key, jsonValue) in jsonArgs) { - keyValuePairs[key] = jsonValue; + inputParameters[key] = jsonValue; } } else @@ -123,13 +130,16 @@ public Dictionary ParseInputParameters(string input) { var tokens = arg.Split("=", 2); var key = tokens[0].Trim(); - object value = (tokens.Length == 1) ? true as object : tokens[1].Trim() as object; - var jsonValue = JObject.FromObject(value).ToString(Newtonsoft.Json.Formatting.None); - keyValuePairs[key] = jsonValue; + var value = (tokens.Length == 1) ? true as object : tokens[1].Trim() as object; + using (var writer = new StringWriter()) + { + Json.Serializer.Serialize(writer, value); + inputParameters[key] = writer.ToString(); + } } } - return keyValuePairs; + return inputParameters; } /// diff --git a/src/Kernel/Magic/Simulate.cs b/src/Kernel/Magic/Simulate.cs index d3cea8b17a..9ca493d685 100644 --- a/src/Kernel/Magic/Simulate.cs +++ b/src/Kernel/Magic/Simulate.cs @@ -18,6 +18,9 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class SimulateMagic : AbstractMagic { + private const string + ParamName_OperationName = "operationName"; + /// /// Constructs a new magic command given a resolver used to find /// operations and functions, and a configuration source used to set @@ -55,15 +58,16 @@ public override ExecutionResult Run(string input, IChannel channel) => /// public async Task RunAsync(string input, IChannel channel) { - var (name, args) = ParseInput(input); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_OperationName); + var name = inputParameters.DecodeParameter(ParamName_OperationName); var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol; if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}"); using var qsim = new QuantumSimulator() .WithJupyterDisplay(channel, ConfigurationSource) .WithStackTraceDisplay(channel); - var value = await symbol.Operation.RunAsync(qsim, args); + var value = await symbol.Operation.RunAsync(qsim, inputParameters); return value.ToExecutionResult(); } } From a85c62ac4e9955897bd3b923b1177bd3421c360b Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 13 May 2020 10:13:03 -0400 Subject: [PATCH 09/13] Minor updates to a few magics --- src/AzureClient/Magic/ConnectMagic.cs | 17 ++++++++++++----- src/AzureClient/Magic/StatusMagic.cs | 16 ++++++++++------ src/AzureClient/Magic/TargetMagic.cs | 4 ++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 82cd888601..ce69542949 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -35,8 +35,8 @@ public ConnectMagic(IAzureClient azureClient) : Summary = "Connects to an Azure workspace or displays current connection status.", Description = @" This magic command allows for connecting to an Azure Quantum workspace - as specified by a valid connection string OR a valid combination of - subscription ID, resource group name, and workspace name. + as specified by a valid subscription ID, resource group name, workspace name, + and storage account connection string. ".Dedent(), Examples = new[] { @@ -49,9 +49,12 @@ as specified by a valid connection string OR a valid combination of ".Dedent(), $@" - Connect to an Azure Quantum workspace using a connection string: + Connect to an Azure Quantum workspace: ``` - In []: %connect {ParamName_StorageAccountConnectionString}=CONNECTION_STRING + In []: %connect {ParamName_SubscriptionId}=SUBSCRIPTION_ID + {ParamName_ResourceGroupName}=RESOURCE_GROUP_NAME + {ParamName_WorkspaceName}=WORKSPACE_NAME + {ParamName_StorageAccountConnectionString}=CONNECTION_STRING Out[]: Connected to WORKSPACE_NAME ``` ".Dedent(), @@ -59,7 +62,11 @@ as specified by a valid connection string OR a valid combination of $@" Connect to an Azure Quantum workspace and force a credential prompt: ``` - In []: %connect {ParamName_Login} {ParamName_StorageAccountConnectionString}=CONNECTION_STRING + In []: %connect {ParamName_Login} + {ParamName_SubscriptionId}=SUBSCRIPTION_ID + {ParamName_ResourceGroupName}=RESOURCE_GROUP_NAME + {ParamName_WorkspaceName}=WORKSPACE_NAME + {ParamName_StorageAccountConnectionString}=CONNECTION_STRING 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 WORKSPACE_NAME diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index 3dbe9932a6..a6c511e608 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -16,6 +17,9 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class StatusMagic : AzureClientMagicBase { + private const string + ParamName_JobId = "jobId"; + /// /// Constructs a new magic command given an IAzureClient object. /// @@ -28,8 +32,8 @@ public StatusMagic(IAzureClient azureClient) : Description = @" This magic command allows for displaying status of jobs in the current Azure Quantum workspace. If a valid job ID is provided as an argument, the - detailed status of that job will be displayed; otherwise, a list of jobs - in the current workspace will be displayed. + detailed status of that job will be displayed; otherwise, a list of all jobs + created in the current session will be displayed. ".Dedent(), Examples = new[] { @@ -42,7 +46,7 @@ in the current workspace will be displayed. ".Dedent(), @" - Print status about all jobs in the current Azure Quantum workspace: + Print status about all jobs created in the current session: ``` In []: %status Out[]: @@ -57,10 +61,10 @@ in the current workspace will be displayed. /// public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = ParseInputParameters(input); - if (keyValuePairs.Keys.Count > 0) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_JobId); + if (inputParameters.ContainsKey(ParamName_JobId)) { - var jobId = keyValuePairs.Keys.First(); + string jobId = inputParameters.DecodeParameter(ParamName_JobId); return await AzureClient.PrintJobStatusAsync(channel, jobId); } diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 5e63c5b167..2cef689222 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -64,9 +64,9 @@ available in the workspace. public override async Task RunAsync(string input, IChannel channel) { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_TargetName); - if (inputParameters.Count() > 0) + if (inputParameters.ContainsKey(ParamName_TargetName)) { - var targetName = inputParameters.DecodeParameter(ParamName_TargetName); + string targetName = inputParameters.DecodeParameter(ParamName_TargetName); return await AzureClient.SetActiveTargetAsync(channel, targetName); } From cacb452dd4659f8cfaba45807d03a102cb779324 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 18 May 2020 17:10:11 -0400 Subject: [PATCH 10/13] Integrate latest Azure.Quantum.Client package --- src/AzureClient/AzureClient.cs | 50 +++++++++++----------- src/AzureClient/AzureClient.csproj | 4 +- src/AzureClient/Extensions.cs | 69 ++++++++++++++++-------------- src/Core/Core.csproj | 6 +-- src/Tool/appsettings.json | 20 ++++----- 5 files changed, 76 insertions(+), 73 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 639764b426..c67a9c0346 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -14,6 +14,7 @@ using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; using Microsoft.Quantum.Simulation.Core; +using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -23,8 +24,8 @@ public class AzureClient : IAzureClient private string ConnectionString { get; set; } private string ActiveTargetName { get; set; } private AuthenticationResult? AuthenticationResult { get; set; } - private IJobsClient? QuantumClient { get; set; } - private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; } + private IQuantumClient? QuantumClient { get; set; } + private Azure.Quantum.Workspace? ActiveWorkspace { get; set; } /// /// Creates an AzureClient object. @@ -97,7 +98,7 @@ public async Task ConnectAsync( var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken); - QuantumClient = new JobsClient(); // TODO: pass (credentials) after updating package. + QuantumClient = new QuantumClient(credentials); QuantumClient.SubscriptionId = subscriptionId; QuantumClient.ResourceGroupName = resourceGroupName; QuantumClient.WorkspaceName = workspaceName; @@ -108,8 +109,8 @@ public async Task ConnectAsync( try { - var jobsList = await QuantumClient.ListJobsAsync(); // QuantumClient.Jobs.ListAsync(); - channel.Stdout($"Found {jobsList.Value.Count()} jobs in Azure Quantum workspace {workspaceName}"); + var jobsList = await QuantumClient.Jobs.ListAsync(); + channel.Stdout($"Found {jobsList.Count()} jobs in Azure Quantum workspace {workspaceName}"); } catch (Exception e) { @@ -117,18 +118,18 @@ public async Task ConnectAsync( return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } - return ActiveWorkspace.ToJupyterTable().ToExecutionResult(); + return QuantumClient.ToJupyterTable().ToExecutionResult(); } /// public async Task PrintConnectionStatusAsync(IChannel channel) { - if (ActiveWorkspace == null) + if (QuantumClient == null) { return AzureClientError.NotConnected.ToExecutionResult(); } - return ActiveWorkspace.ToJupyterTable().ToExecutionResult(); + return QuantumClient.ToJupyterTable().ToExecutionResult(); } /// @@ -137,7 +138,7 @@ public async Task SubmitJobAsync( IOperationResolver operationResolver, string operationName) { - if (QuantumClient == null) + if (ActiveWorkspace == null) { channel.Stderr("Must call %connect before submitting a job."); return AzureClientError.NotConnected.ToExecutionResult(); @@ -155,18 +156,18 @@ public async Task SubmitJobAsync( return AzureClientError.NoOperationName.ToExecutionResult(); } - // TODO: Generate the appropriate EntryPointInfo for the given operation. var operationInfo = operationResolver.Resolve(operationName); var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); var entryPointInput = QVoid.Instance; + var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTargetName, ConnectionString); + if (machine == null) + { + channel.Stderr($"Could not find an execution target for target {ActiveTargetName}."); + return AzureClientError.NoTarget.ToExecutionResult(); + } - // TODO: Create the appropriate QuantumMachine object using ActiveTargetName - var jobStorageHelper = new JobStorageHelper(ConnectionString); - //var machine = new QuantumMachine(ActiveTargetName, Workspace, jobStorageHelper); - //var submissionContext = new QuantumMachine.SubmissionContext(); - //var job = await machine.SubmitAsync(entryPointInfo, entryPointInput, submissionContext); - //return job.Id.ToExecutionResult(); - return operationName.ToExecutionResult(); + var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); + return job.ToJupyterTable().ToExecutionResult(); } /// @@ -196,15 +197,14 @@ public async Task PrintActiveTargetAsync( public async Task PrintTargetListAsync( IChannel channel) { - if (ActiveWorkspace == null) + if (QuantumClient == null) { channel.Stderr("Must call %connect before listing targets."); return AzureClientError.NotConnected.ToExecutionResult(); } - // TODO: Get the list of targets from the updated Azure.Quantum.Client API - //return ActiveWorkspace.Targets.ToJupyterTable().ToExecutionResult(); - return AzureClientError.NoTarget.ToExecutionResult(); + var providersStatus = await QuantumClient.Providers.GetStatusAsync(); + return providersStatus.ToJupyterTable().ToExecutionResult(); } /// @@ -218,7 +218,7 @@ public async Task PrintJobStatusAsync( return AzureClientError.NotConnected.ToExecutionResult(); } - var jobDetails = await QuantumClient.GetJobAsync(jobId); // QuantumClient.Jobs.GetAsync(jobId); + var jobDetails = await QuantumClient.Jobs.GetAsync(jobId); if (jobDetails == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -238,14 +238,14 @@ public async Task PrintJobListAsync( return AzureClientError.NotConnected.ToExecutionResult(); } - var jobsList = await QuantumClient.ListJobsAsync(); // QuantumClient.Jobs.ListAsync(); - if (jobsList.Value == null || jobsList.Value.Count() == 0) + var jobsList = await QuantumClient.Jobs.ListAsync(); + if (jobsList == null || jobsList.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); return AzureClientError.JobNotFound.ToExecutionResult(); } - return jobsList.Value.ToJupyterTable().ToExecutionResult(); + return jobsList.ToJupyterTable().ToExecutionResult(); } } } diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index 32bae3bed8..3b9d29e070 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,9 +13,7 @@ - - - + diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index c0245d16c0..b07fa4764b 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -8,10 +8,12 @@ using System.ComponentModel; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Quantum.Client; using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; using Microsoft.Quantum.QsCompiler.Serialization; +using Microsoft.Quantum.Runtime; using Newtonsoft.Json; namespace Microsoft.Quantum.IQSharp.AzureClient @@ -74,47 +76,50 @@ internal static Table ToJupyterTable(this IEnumerable jo { Columns = new List<(string, Func)> { - ("Id", jobDetails => jobDetails.Id), - ("ProviderId", jobDetails => jobDetails.ProviderId), - ("Status", jobDetails => jobDetails.Status) + ("JobId", jobDetails => jobDetails.Id), + ("JobName", jobDetails => jobDetails.Name), + ("JobStatus", jobDetails => jobDetails.Status), + ("Provider", jobDetails => jobDetails.ProviderId), + ("Target", jobDetails => jobDetails.Target), }, Rows = jobsList.ToList() }; - internal static Table ToJupyterTable(this Azure.Quantum.IWorkspace workspace) => - new List { workspace }.ToJupyterTable(); - - internal static Table ToJupyterTable(this IEnumerable workspacesList) => - new Table + internal static Table ToJupyterTable(this IQuantumMachineJob job) => + new Table { - Columns = new List<(string, Func)> + Columns = new List<(string, Func)> { - // TODO: Uncomment this after updating the Azure.Quantum.Client API - //("Name", workspace => workspace.Name), - //("Type", workspace => workspace.Type), - //("Location", workspace => workspace.Location), - ("TEMP_AsString", workspace => workspace.ToString()), + ("JobId", job => job.Id), + ("JobStatus", job => job.Status), + ("JobUri", job => job.Uri.ToString()), }, - Rows = workspacesList.ToList() + Rows = new List() { job } }; - // TODO: Implement this for providers and targets once they are exposed - // through the Azure.Quantum.Client API. - - //internal static Table ToJupyterTable(this Provider provider) => - // new List { provider }.ToJupyterTable(); + internal static Table ToJupyterTable(this IQuantumClient quantumClient) => + new Table + { + Columns = new List<(string, Func)> + { + ("SubscriptionId", quantumClient => quantumClient.SubscriptionId), + ("ResourceGroupName", quantumClient => quantumClient.ResourceGroupName), + ("WorkspaceName", quantumClient => quantumClient.WorkspaceName), + }, + Rows = new List() { quantumClient } + }; - //internal static Table ToJupyterTable(this IEnumerable providersList) => - // new Table - // { - // Columns = new List<(string, Func)> - // { - // ("Name", provider => provider.ApplicationName), - // ("ProviderId", provider => provider.ProviderId), - // ("ProviderSku", provider => provider.ProviderSku), - // ("ProvisioningState", provider => provider.ProvisioningState) - // }, - // Rows = providersList.ToList() - // }; + internal static Table ToJupyterTable(this IEnumerable providerStatusList) => + new Table + { + Columns = new List<(string, Func)> + { + ("TargetId", target => target.Id), + ("CurrentAvailability", target => target.CurrentAvailability), + ("AverageQueueTime", target => target.AverageQueueTime.ToString()), + ("StatusPage", target => target.StatusPage), + }, + Rows = providerStatusList.SelectMany(provider => provider.Targets).ToList() + }; } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 249d061dcd..b67e57c334 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 44d378812c..5c8675d093 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,18 +6,18 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2004.2825", + "Microsoft.Quantum.Compiler::0.11.2005.1420-beta", - "Microsoft.Quantum.CsharpGeneration::0.11.2004.2825", - "Microsoft.Quantum.Development.Kit::0.11.2004.2825", - "Microsoft.Quantum.Simulators::0.11.2004.2825", - "Microsoft.Quantum.Xunit::0.11.2004.2825", + "Microsoft.Quantum.CsharpGeneration::0.11.2005.1420-beta", + "Microsoft.Quantum.Development.Kit::0.11.2005.1420-beta", + "Microsoft.Quantum.Simulators::0.11.2005.1420-beta", + "Microsoft.Quantum.Xunit::0.11.2005.1420-beta", - "Microsoft.Quantum.Standard::0.11.2004.2825", - "Microsoft.Quantum.Chemistry::0.11.2004.2825", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2004.2825", - "Microsoft.Quantum.Numerics::0.11.2004.2825", + "Microsoft.Quantum.Standard::0.11.2005.1420-beta", + "Microsoft.Quantum.Chemistry::0.11.2005.1420-beta", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2005.1420-beta", + "Microsoft.Quantum.Numerics::0.11.2005.1420-beta", - "Microsoft.Quantum.Research::0.11.2004.2825" + "Microsoft.Quantum.Research::0.11.2005.1420-beta" ] } From 5386fddc7324b7b46f531a3dc3adb6a49e49f915 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 18 May 2020 17:21:03 -0400 Subject: [PATCH 11/13] Minor cleanup --- src/AzureClient/AzureClient.cs | 2 -- src/Python/qsharp/azure.py | 10 +--------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c67a9c0346..8fe083283d 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -9,12 +9,10 @@ using System.IO; using System.Threading.Tasks; using Microsoft.Azure.Quantum.Client; -using Microsoft.Azure.Quantum.Storage; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; using Microsoft.Quantum.Simulation.Core; -using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient { diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index 3cec5bcb81..2182a20b30 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -29,9 +29,7 @@ 'connect', 'target', 'submit', - 'execute', - 'status', - 'output' + 'status' ] ## FUNCTIONS ## @@ -45,11 +43,5 @@ def target(name : str = '', **params) -> Any: def submit(op, **params) -> Any: return qsharp.client._execute_callable_magic("submit", op, raise_on_stderr=False, **params) -def execute(op, **params) -> Any: - return qsharp.client._execute_callable_magic("execute", op, raise_on_stderr=False, **params) - def status(jobId : str = '', **params) -> Any: return qsharp.client._execute_magic(f"status {jobId}", raise_on_stderr=False, **params) - -def output(jobId : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"output {jobId}", raise_on_stderr=False, **params) From e7efc5e173b2973f265e0176487264b065fceb04 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 19 May 2020 14:34:05 -0400 Subject: [PATCH 12/13] Improvements to address PR comments --- src/AzureClient/AzureClient.cs | 75 ++++++------------- src/AzureClient/Extensions.cs | 10 +-- src/AzureClient/IAzureClient.cs | 29 +++---- src/AzureClient/Magic/AzureClientMagicBase.cs | 15 ++-- src/AzureClient/Magic/ConnectMagic.cs | 40 +++++----- src/AzureClient/Magic/StatusMagic.cs | 8 +- src/AzureClient/Magic/TargetMagic.cs | 8 +- src/AzureClient/Resources.cs | 18 ++--- src/Jupyter/Extensions.cs | 8 +- src/Kernel/Magic/Simulate.cs | 6 +- src/Tests/AzureClientMagicTests.cs | 7 -- src/Tests/AzureClientTests.cs | 4 - 12 files changed, 91 insertions(+), 137 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 8fe083283d..371ab20c95 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -19,21 +19,12 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { - private string ConnectionString { get; set; } - private string ActiveTargetName { get; set; } + private string ConnectionString { get; set; } = string.Empty; + private string ActiveTargetName { get; set; } = string.Empty; private AuthenticationResult? AuthenticationResult { get; set; } private IQuantumClient? QuantumClient { get; set; } private Azure.Quantum.Workspace? ActiveWorkspace { get; set; } - /// - /// Creates an AzureClient object. - /// - public AzureClient() - { - ConnectionString = string.Empty; - ActiveTargetName = string.Empty; - } - /// public async Task ConnectAsync( IChannel channel, @@ -41,7 +32,7 @@ public async Task ConnectAsync( string resourceGroupName, string workspaceName, string storageAccountConnectionString, - bool forceLogin = false) + bool forceLoginPrompt = false) { ConnectionString = storageAccountConnectionString; @@ -64,8 +55,8 @@ public async Task ConnectAsync( var scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" }; - bool promptForLogin = forceLogin; - if (!promptForLogin) + bool shouldShowLoginPrompt = forceLoginPrompt; + if (!shouldShowLoginPrompt) { try { @@ -75,11 +66,11 @@ public async Task ConnectAsync( } catch (MsalUiRequiredException) { - promptForLogin = true; + shouldShowLoginPrompt = true; } } - if (promptForLogin) + if (shouldShowLoginPrompt) { AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode(scopes, deviceCodeResult => @@ -95,12 +86,12 @@ public async Task ConnectAsync( } var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken); - - QuantumClient = new QuantumClient(credentials); - QuantumClient.SubscriptionId = subscriptionId; - QuantumClient.ResourceGroupName = resourceGroupName; - QuantumClient.WorkspaceName = workspaceName; - + QuantumClient = new QuantumClient(credentials) + { + SubscriptionId = subscriptionId, + ResourceGroupName = resourceGroupName, + WorkspaceName = workspaceName + }; ActiveWorkspace = new Azure.Quantum.Workspace( QuantumClient.SubscriptionId, QuantumClient.ResourceGroupName, QuantumClient.WorkspaceName, AuthenticationResult?.AccessToken); @@ -108,7 +99,7 @@ public async Task ConnectAsync( try { var jobsList = await QuantumClient.Jobs.ListAsync(); - channel.Stdout($"Found {jobsList.Count()} jobs in Azure Quantum workspace {workspaceName}"); + channel.Stdout($"Successfully connected to Azure Quantum workspace {workspaceName}."); } catch (Exception e) { @@ -120,15 +111,10 @@ public async Task ConnectAsync( } /// - public async Task PrintConnectionStatusAsync(IChannel channel) - { - if (QuantumClient == null) - { - return AzureClientError.NotConnected.ToExecutionResult(); - } - - return QuantumClient.ToJupyterTable().ToExecutionResult(); - } + public async Task PrintConnectionStatusAsync(IChannel channel) => + QuantumClient == null + ? AzureClientError.NotConnected.ToExecutionResult() + : QuantumClient.ToJupyterTable().ToExecutionResult(); /// public async Task SubmitJobAsync( @@ -138,19 +124,19 @@ public async Task SubmitJobAsync( { if (ActiveWorkspace == null) { - channel.Stderr("Must call %connect before submitting a job."); + channel.Stderr("Please call %connect before submitting a job."); return AzureClientError.NotConnected.ToExecutionResult(); } if (ActiveTargetName == null) { - channel.Stderr("Must call %target before submitting a job."); + channel.Stderr("Please call %target before submitting a job."); return AzureClientError.NoTarget.ToExecutionResult(); } if (string.IsNullOrEmpty(operationName)) { - channel.Stderr("Must pass a valid Q# operation name to %submit."); + channel.Stderr("Please pass a valid Q# operation name to %submit."); return AzureClientError.NoOperationName.ToExecutionResult(); } @@ -178,26 +164,13 @@ public async Task SetActiveTargetAsync( return $"Active target is now {ActiveTargetName}".ToExecutionResult(); } - /// - public async Task PrintActiveTargetAsync( - IChannel channel) - { - if (string.IsNullOrEmpty(ActiveTargetName)) - { - channel.Stderr("No active target has been set for the current Azure Quantum workspace."); - return AzureClientError.NoTarget.ToExecutionResult(); - } - - return $"Active target is {ActiveTargetName}.".ToExecutionResult(); - } - /// public async Task PrintTargetListAsync( IChannel channel) { if (QuantumClient == null) { - channel.Stderr("Must call %connect before listing targets."); + channel.Stderr("Please call %connect before listing targets."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -212,7 +185,7 @@ public async Task PrintJobStatusAsync( { if (QuantumClient == null) { - channel.Stderr("Must call %connect before getting job status."); + channel.Stderr("Please call %connect before getting job status."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -232,7 +205,7 @@ public async Task PrintJobListAsync( { if (QuantumClient == null) { - channel.Stderr("Must call %connect before listing jobs."); + channel.Stderr("Please call %connect before listing jobs."); return AzureClientError.NotConnected.ToExecutionResult(); } diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index b07fa4764b..7a8e950951 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -12,9 +12,7 @@ using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.QsCompiler.Serialization; using Microsoft.Quantum.Runtime; -using Newtonsoft.Json; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -32,7 +30,7 @@ public static void AddAzureClient(this IServiceCollection services) } /// - /// Encapsulates a given AzureClientError as the result of an execution. + /// Encapsulates a given as the result of an execution. /// /// /// The result of an IAzureClient API call. @@ -45,8 +43,8 @@ public static ExecutionResult ToExecutionResult(this AzureClientError azureClien }; /// - /// Returns the string value of the DescriptionAttribute for the given - /// AzureClientError enumeration value. + /// Returns the string value of the for the given + /// enumeration value. /// /// /// @@ -60,7 +58,7 @@ public static string ToDescription(this AzureClientError azureClientError) } /// - /// Encapsulates a given AzureClientError as the result of an execution. + /// Encapsulates a given as the result of an execution. /// /// /// A task which will return the result of an IAzureClient API call. diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 2e896201bf..d98c420431 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -3,60 +3,57 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Jupyter.Core; -using System.Threading.Tasks; using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; namespace Microsoft.Quantum.IQSharp.AzureClient { /// - /// Describes possible error results from IAzureClient methods. + /// Describes possible error results from methods. /// public enum AzureClientError { /// /// Method completed with an unknown error. /// - [Description(Resources.AzureClientError_UnknownError)] + [Description(Resources.AzureClientErrorUnknownError)] UnknownError = 0, /// /// No connection has been made to any Azure Quantum workspace. /// - [Description(Resources.AzureClientError_NotConnected)] + [Description(Resources.AzureClientErrorNotConnected)] NotConnected = 1, /// /// A target has not yet been configured for job submission. /// - [Description(Resources.AzureClientError_NoTarget)] + [Description(Resources.AzureClientErrorNoTarget)] NoTarget = 2, /// /// A job meeting the specified criteria was not found. /// - [Description(Resources.AzureClientError_JobNotFound)] + [Description(Resources.AzureClientErrorJobNotFound)] JobNotFound = 3, /// /// No Q# operation name was provided where one was required. /// - [Description(Resources.AzureClientError_NoOperationName)] + [Description(Resources.AzureClientErrorNoOperationName)] NoOperationName = 4, /// /// Authentication with the Azure service failed. /// - [Description(Resources.AzureClientError_AuthenticationFailed)] + [Description(Resources.AzureClientErrorAuthenticationFailed)] AuthenticationFailed = 5, /// /// A workspace meeting the specified criteria was not found. /// - [Description(Resources.AzureClientError_WorkspaceNotFound)] + [Description(Resources.AzureClientErrorWorkspaceNotFound)] WorkspaceNotFound = 6, } @@ -97,12 +94,6 @@ public Task SubmitJobAsync( public Task SetActiveTargetAsync( IChannel channel, string targetName); - - /// - /// Prints the specified target for job submission. - /// - public Task PrintActiveTargetAsync( - IChannel channel); /// /// Prints the list of targets currently provisioned in the current workspace. diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs index 9a2071ea0b..f7eac3b5ce 100644 --- a/src/AzureClient/Magic/AzureClientMagicBase.cs +++ b/src/AzureClient/Magic/AzureClientMagicBase.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; using System.Threading.Tasks; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.IQSharp.Common; using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.AzureClient @@ -24,6 +24,9 @@ public abstract class AzureClientMagicBase : AbstractMagic /// Constructs the Azure Client magic command with the specified keyword /// and documentation. /// + /// The object used to interact with Azure. + /// The name used to invoke the magic command. + /// Documentation describing the usage of this magic command. public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentation docs): base(keyword, docs) { diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index ce69542949..4a325a923b 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -18,11 +18,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient public class ConnectMagic : AzureClientMagicBase { private const string - ParamName_Login = "login", - ParamName_StorageAccountConnectionString = "storageAccountConnectionString", - ParamName_SubscriptionId = "subscriptionId", - ParamName_ResourceGroupName = "resourceGroupName", - ParamName_WorkspaceName = "workspaceName"; + ParameterNameLogin = "login", + ParameterNameStorageAccountConnectionString = "storageAccountConnectionString", + ParameterNameSubscriptionId = "subscriptionId", + ParameterNameResourceGroupName = "resourceGroupName", + ParameterNameWorkspaceName = "workspaceName"; /// /// Constructs a new magic command given an IAzureClient object. @@ -51,10 +51,10 @@ and storage account connection string. $@" Connect to an Azure Quantum workspace: ``` - In []: %connect {ParamName_SubscriptionId}=SUBSCRIPTION_ID - {ParamName_ResourceGroupName}=RESOURCE_GROUP_NAME - {ParamName_WorkspaceName}=WORKSPACE_NAME - {ParamName_StorageAccountConnectionString}=CONNECTION_STRING + In []: %connect {ParameterNameSubscriptionId}=SUBSCRIPTION_ID + {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME + {ParameterNameWorkspaceName}=WORKSPACE_NAME + {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING Out[]: Connected to WORKSPACE_NAME ``` ".Dedent(), @@ -62,16 +62,16 @@ and storage account connection string. $@" Connect to an Azure Quantum workspace and force a credential prompt: ``` - In []: %connect {ParamName_Login} - {ParamName_SubscriptionId}=SUBSCRIPTION_ID - {ParamName_ResourceGroupName}=RESOURCE_GROUP_NAME - {ParamName_WorkspaceName}=WORKSPACE_NAME - {ParamName_StorageAccountConnectionString}=CONNECTION_STRING + In []: %connect {ParameterNameLogin} + {ParameterNameSubscriptionId}=SUBSCRIPTION_ID + {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME + {ParameterNameWorkspaceName}=WORKSPACE_NAME + {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING 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 WORKSPACE_NAME ``` - Use the `{ParamName_Login}` option if you want to bypass any saved or cached + Use the `{ParameterNameLogin}` option if you want to bypass any saved or cached credentials when connecting to Azure. ".Dedent() } @@ -85,16 +85,16 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input); - var storageAccountConnectionString = inputParameters.DecodeParameter(ParamName_StorageAccountConnectionString); + var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString); if (string.IsNullOrEmpty(storageAccountConnectionString)) { return await AzureClient.PrintConnectionStatusAsync(channel); } - var subscriptionId = inputParameters.DecodeParameter(ParamName_SubscriptionId); - var resourceGroupName = inputParameters.DecodeParameter(ParamName_ResourceGroupName); - var workspaceName = inputParameters.DecodeParameter(ParamName_WorkspaceName); - var forceLogin = inputParameters.DecodeParameter(ParamName_Login); + var subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId); + var resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName); + var workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName); + var forceLogin = inputParameters.DecodeParameter(ParameterNameLogin, defaultValue: false); return await AzureClient.ConnectAsync( channel, subscriptionId, diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index a6c511e608..d39c6927da 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -18,7 +18,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient public class StatusMagic : AzureClientMagicBase { private const string - ParamName_JobId = "jobId"; + ParameterNameJobId = "jobId"; /// /// Constructs a new magic command given an IAzureClient object. @@ -61,10 +61,10 @@ created in the current session will be displayed. /// public override async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_JobId); - if (inputParameters.ContainsKey(ParamName_JobId)) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameJobId); + if (inputParameters.ContainsKey(ParameterNameJobId)) { - string jobId = inputParameters.DecodeParameter(ParamName_JobId); + string jobId = inputParameters.DecodeParameter(ParameterNameJobId); return await AzureClient.PrintJobStatusAsync(channel, jobId); } diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 2cef689222..4108009cc6 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -18,7 +18,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient public class TargetMagic : AzureClientMagicBase { private const string - ParamName_TargetName = "name"; + ParameterNameTargetName = "name"; /// /// Constructs a new magic command given an IAzureClient object. @@ -63,10 +63,10 @@ available in the workspace. /// public override async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_TargetName); - if (inputParameters.ContainsKey(ParamName_TargetName)) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetName); + if (inputParameters.ContainsKey(ParameterNameTargetName)) { - string targetName = inputParameters.DecodeParameter(ParamName_TargetName); + string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); return await AzureClient.SetActiveTargetAsync(channel, targetName); } diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index 829ae77acc..6c3de47cc0 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; +#nullable enable namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -9,25 +7,25 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// internal static class Resources { - public const string AzureClientError_UnknownError = + public const string AzureClientErrorUnknownError = "An unknown error occurred."; - public const string AzureClientError_NotConnected = + public const string AzureClientErrorNotConnected = "Not connected to any Azure Quantum workspace."; - public const string AzureClientError_NoTarget = + public const string AzureClientErrorNoTarget = "No execution target has been configured for Azure Quantum job submission."; - public const string AzureClientError_JobNotFound = + public const string AzureClientErrorJobNotFound = "No job with the given ID was found in the current Azure Quantum workspace."; - public const string AzureClientError_NoOperationName = + public const string AzureClientErrorNoOperationName = "No Q# operation name was specified for Azure Quantum job submission."; - public const string AzureClientError_AuthenticationFailed = + public const string AzureClientErrorAuthenticationFailed = "Failed to authenticate to the specified Azure Quantum workspace."; - public const string AzureClientError_WorkspaceNotFound = + public const string AzureClientErrorWorkspaceNotFound = "No Azure Quantum workspace was found that matches the specified criteria."; } } diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs index b0b4a6b22f..b016d9c328 100644 --- a/src/Jupyter/Extensions.cs +++ b/src/Jupyter/Extensions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -179,13 +181,13 @@ public static string Dedent(this string text) /// /// Retrieves and JSON-decodes the value for the given parameter name. /// - public static T DecodeParameter(this Dictionary parameters, string parameterName) + public static T DecodeParameter(this Dictionary parameters, string parameterName, T defaultValue = default) { if (!parameters.TryGetValue(parameterName, out string parameterValue)) { - return default(T); + return defaultValue; } - return (T)(JsonConvert.DeserializeObject(parameterValue) ?? default(T)); + return (T)(JsonConvert.DeserializeObject(parameterValue)) ?? defaultValue; } } } diff --git a/src/Kernel/Magic/Simulate.cs b/src/Kernel/Magic/Simulate.cs index 9ca493d685..2b94eb9b06 100644 --- a/src/Kernel/Magic/Simulate.cs +++ b/src/Kernel/Magic/Simulate.cs @@ -19,7 +19,7 @@ namespace Microsoft.Quantum.IQSharp.Kernel public class SimulateMagic : AbstractMagic { private const string - ParamName_OperationName = "operationName"; + ParameterNameOperationName = "operationName"; /// /// Constructs a new magic command given a resolver used to find @@ -58,9 +58,9 @@ public override ExecutionResult Run(string input, IChannel channel) => /// public async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParamName_OperationName); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); - var name = inputParameters.DecodeParameter(ParamName_OperationName); + var name = inputParameters.DecodeParameter(ParameterNameOperationName); var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol; if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}"); diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index c7846ada42..0a48c1e5b7 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -119,7 +119,6 @@ internal enum AzureClientAction Connect, SetActiveTarget, SubmitJob, - PrintActiveTarget, PrintConnectionStatus, PrintJobList, PrintJobStatus, @@ -156,12 +155,6 @@ public async Task ConnectAsync(IChannel channel, string subscri return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task PrintActiveTargetAsync(IChannel channel) - { - LastAction = AzureClientAction.PrintActiveTarget; - return ExecuteStatus.Ok.ToExecutionResult(); - } - public async Task PrintConnectionStatusAsync(IChannel channel) { LastAction = AzureClientAction.PrintConnectionStatus; diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index d6668db4f5..6c5eb88334 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -33,10 +33,6 @@ public void TestTargets() var result = azureClient.SetActiveTargetAsync(new MockChannel(), targetName).GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Ok); - result = azureClient.PrintActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Ok); - Assert.IsNotNull(result.Output); - result = azureClient.PrintTargetListAsync(new MockChannel()).GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); } From 6296c793385f82ebbd9ed52c70b07cdf3f67efbb Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 19 May 2020 14:49:41 -0400 Subject: [PATCH 13/13] Minor change to fix syntax highlighting --- src/Tests/AzureClientMagicTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 0a48c1e5b7..730554ede8 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -55,8 +55,7 @@ public void TestConnectMagic() // valid input with forced login connectMagic.Test( - @$"login - subscriptionId={subscriptionId} + @$"login subscriptionId={subscriptionId} resourceGroupName={resourceGroupName} workspaceName={workspaceName} storageAccountConnectionString={storageAccountConnectionString}");