From 039e2e98d1158eae4021468a84ae8ce4f6e46f9c Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Wed, 2 Aug 2023 17:54:08 -0700 Subject: [PATCH] gh-418 Update CLI to support --plugins and update API doc Signed-off-by: Victor Chang --- docs/api/rest/config.md | 24 ++++++++++++++-- docs/setup/setup.md | 13 +++++++++ src/Api/MonaiApplicationEntity.cs | 3 ++ src/CLI/Commands/AetCommand.cs | 19 +++++++++++-- src/CLI/Logging/Log.cs | 3 ++ src/CLI/Test/AetCommandTest.cs | 46 +++++++++++++++++++++++++++++-- 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/docs/api/rest/config.md b/docs/api/rest/config.md index ec7735ac5..9aa45867b 100644 --- a/docs/api/rest/config.md +++ b/docs/api/rest/config.md @@ -54,7 +54,11 @@ curl --location --request GET 'http://localhost:5000/config/ae' "grouping": "0020,000D", "timeout": 5, "ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"], - "allowedSopClasses": ["1.2.840.10008.5.1.4.1.1.1.2"] + "allowedSopClasses": ["1.2.840.10008.5.1.4.1.1.1.2"], + "pluginAssemblies": [ + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginAddWorkflow, Monai.Deploy.InformaticsGateway.Test.Plugins", + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.Plugins" + ] }, { "name": "liver-seg", @@ -151,6 +155,10 @@ curl --location --request POST 'http://localhost:5000/config/ae/' \ "timeout": 5, "workflows": [ "3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e" + ], + "pluginAssemblies": [ + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginAddWorkflow, Monai.Deploy.InformaticsGateway.Test.Plugins", + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.Plugins" ] } }' @@ -162,7 +170,11 @@ curl --location --request POST 'http://localhost:5000/config/ae/' \ { "name": "breast-tumor", "aeTitle": "BREASTV1", - "workflows": ["3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e"] + "workflows": ["3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e"], + "pluginAssemblies": [ + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginAddWorkflow, Monai.Deploy.InformaticsGateway.Test.Plugins", + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.Plugins" + ] } ``` @@ -210,6 +222,10 @@ curl --location --request PUT 'http://localhost:5000/config/ae/' \ "timeout": 3, "workflows": [ "3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e" + ], + "pluginAssemblies": [ + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginAddWorkflow, Monai.Deploy.InformaticsGateway.Test.Plugins", + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.Plugins" ] } }' @@ -222,6 +238,10 @@ curl --location --request PUT 'http://localhost:5000/config/ae/' \ "name": "breast-tumor", "aeTitle": "BREASTV1", "workflows": ["3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e"], + "pluginAssemblies": [ + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginAddWorkflow, Monai.Deploy.InformaticsGateway.Test.Plugins", + "Monai.Deploy.InformaticsGateway.Test.Plugins.TestInputDataPluginModifyDicomFile, Monai.Deploy.InformaticsGateway.Test.Plugins" + ], "timeout": 3 } ``` diff --git a/docs/setup/setup.md b/docs/setup/setup.md index 70617aa87..4eea52ec8 100644 --- a/docs/setup/setup.md +++ b/docs/setup/setup.md @@ -311,6 +311,19 @@ mig-cli aet add -a BrainAET -grouping 0020,000E, -t 30 The command creates a new listening AE Title with AE Title `BrainAET`. The listening AE Title will group instances by the Series Instance UID (0020,000E) with a timeout value of 30 seconds. + +### Optional: Input Data Plug-ins + +Each listening AE Title may be configured with zero or more plug-ins to maniulate incoming DICOM files before saving to the storage +service and dispatching a workflow request. To include input data plug-ins, first create your plug-ins by implementing the +[IInputDataPlugin](xref:Monai.Deploy.InformaticsGateway.Api.IInputDataPlugin) interface and then use `-p` argument with the fully +qualified type name with the `mig-cli aet add` command. For example, the following command adds `MyNamespace.AnonymizePlugin` +and `MyNamespace.FixSeriesData` plug-ins from the `MyNamespace.Plugins` assembly file. + +```bash +mig-cli aet add -a BrainAET -grouping 0020,000E, -t 30 -p "MyNamespace.AnonymizePlugin, MyNamespace.Plugins" "MyNamespace.FixSeriesData, MyNamespace.Plugins" +``` + > [!Note] > `-grouping` is optional, with a default value of 0020,000D. > `-t` is optional, with a default value of 5 seconds. diff --git a/src/Api/MonaiApplicationEntity.cs b/src/Api/MonaiApplicationEntity.cs index 68050a8ee..abf1fba67 100644 --- a/src/Api/MonaiApplicationEntity.cs +++ b/src/Api/MonaiApplicationEntity.cs @@ -72,6 +72,9 @@ public class MonaiApplicationEntity : MongoDBEntityBase /// public List Workflows { get; set; } = default!; + /// + /// Optional list of data input plug-in type names to be executed by the . + /// public List PluginAssemblies { get; set; } = default!; /// diff --git a/src/CLI/Commands/AetCommand.cs b/src/CLI/Commands/AetCommand.cs index 66d29d582..8d8c4a0e0 100644 --- a/src/CLI/Commands/AetCommand.cs +++ b/src/CLI/Commands/AetCommand.cs @@ -97,6 +97,12 @@ private void SetupAddAetCommand() IsRequired = false, }; addCommand.AddOption(allowedSopsOption); + var plugins = new Option>(new string[] { "-p", "--plugins" }, description: "A space separated list of fully qualified type names of the plug-ins (surround each plug-in with double quotes)") + { + AllowMultipleArgumentsPerToken = true, + IsRequired = false, + }; + addCommand.AddOption(plugins); addCommand.Handler = CommandHandler.Create(AddAeTitlehandlerAsync); } @@ -130,6 +136,12 @@ private void SetupEditAetCommand() IsRequired = false, }; addCommand.AddOption(allowedSopsOption); + var plugins = new Option>(new string[] { "-p", "--plugins" }, description: "A space separated list of fully qualified type names of the plug-ins (surround each plug-in with double quotes)") + { + AllowMultipleArgumentsPerToken = true, + IsRequired = false, + }; + addCommand.AddOption(plugins); addCommand.Handler = CommandHandler.Create(EditAeTitleHandlerAsync); } @@ -274,8 +286,7 @@ private async Task AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IH } if (result.AllowedSopClasses.Any()) { - logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses)); - logger.AcceptedSopClassesWarning(); + logger.MonaiAePlugins(string.Join(',', result.AllowedSopClasses)); } } catch (ConfigurationException ex) @@ -330,6 +341,10 @@ private async Task EditAeTitleHandlerAsync(MonaiApplicationEntity entity, I logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses)); logger.AcceptedSopClassesWarning(); } + if (result.AllowedSopClasses.Any()) + { + logger.MonaiAePlugins(string.Join(',', result.AllowedSopClasses)); + } } catch (ConfigurationException ex) { diff --git a/src/CLI/Logging/Log.cs b/src/CLI/Logging/Log.cs index 156d7d761..9c1a3d43f 100644 --- a/src/CLI/Logging/Log.cs +++ b/src/CLI/Logging/Log.cs @@ -187,6 +187,9 @@ public static partial class Log [LoggerMessage(EventId = 30061, Level = LogLevel.Critical, Message = "Error updating SCP Application Entity {aeTitle}: {message}")] public static partial void ErrorUpdatingMonaiApplicationEntity(this ILogger logger, string aeTitle, string message); + [LoggerMessage(EventId = 30062, Level = LogLevel.Information, Message = "\tPlug-ins: {plugins}")] + public static partial void MonaiAePlugins(this ILogger logger, string plugins); + // Docker Runner [LoggerMessage(EventId = 31000, Level = LogLevel.Debug, Message = "Checking for existing {applicationName} ({version}) containers...")] public static partial void CheckingExistingAppContainer(this ILogger logger, string applicationName, string version); diff --git a/src/CLI/Test/AetCommandTest.cs b/src/CLI/Test/AetCommandTest.cs index 86154ba71..636576fef 100644 --- a/src/CLI/Test/AetCommandTest.cs +++ b/src/CLI/Test/AetCommandTest.cs @@ -100,7 +100,7 @@ public async Task AetAdd_Command() { Name = result.CommandResult.Children[0].Tokens[0].Value, AeTitle = result.CommandResult.Children[1].Tokens[0].Value, - Workflows = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList() + Workflows = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(), }; Assert.Equal("MyName", entity.Name); Assert.Equal("MyAET", entity.AeTitle); @@ -123,6 +123,44 @@ public async Task AetAdd_Command() It.IsAny()), Times.Once()); } + [Fact(DisplayName = "aet add comand with plug-ins")] + public async Task AetAdd_Command_WithPlugins() + { + var command = "aet add -n MyName -a MyAET --workflows App MyCoolApp TheApp --plugins \"PluginTypeA\" \"PluginTypeB\""; + var result = _paser.Parse(command); + Assert.Equal(ExitCodes.Success, result.Errors.Count); + + var entity = new MonaiApplicationEntity() + { + Name = result.CommandResult.Children[0].Tokens[0].Value, + AeTitle = result.CommandResult.Children[1].Tokens[0].Value, + Workflows = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(), + PluginAssemblies = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(), + }; + Assert.Equal("MyName", entity.Name); + Assert.Equal("MyAET", entity.AeTitle); + Assert.Collection(entity.Workflows, + item => item.Equals("App"), + item => item.Equals("MyCoolApp"), + item => item.Equals("TheApp")); + Assert.Collection(entity.PluginAssemblies, + item => item.Equals("PluginTypeA"), + item => item.Equals("PluginTypeB")); + + _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Create(It.IsAny(), It.IsAny())) + .ReturnsAsync(entity); + + int exitCode = await _paser.InvokeAsync(command); + + Assert.Equal(ExitCodes.Success, exitCode); + + _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once()); + _informaticsGatewayClient.Verify( + p => p.MonaiScpAeTitle.Create( + It.Is(o => o.AeTitle == entity.AeTitle && o.Name == entity.Name && Enumerable.SequenceEqual(o.Workflows, entity.Workflows)), + It.IsAny()), Times.Once()); + } + [Fact(DisplayName = "aet add comand with allowed & ignored SOP classes")] public async Task AetAdd_Command_AllowedIgnoredSopClasses() { @@ -335,7 +373,7 @@ public async Task AetList_Command_Empty() [Fact(DisplayName = "aet update command")] public async Task AetUpdate_Command() { - var command = "aet update -n MyName --workflows App MyCoolApp TheApp -i A B C -s D E F"; + var command = "aet update -n MyName --workflows App MyCoolApp TheApp -i A B C -s D E F -p PlugInAssemblyA PlugInAssemblyB"; var result = _paser.Parse(command); Assert.Equal(ExitCodes.Success, result.Errors.Count); @@ -346,6 +384,7 @@ public async Task AetUpdate_Command() Workflows = result.CommandResult.Children[1].Tokens.Select(p => p.Value).ToList(), IgnoredSopClasses = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(), AllowedSopClasses = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(), + PluginAssemblies = result.CommandResult.Children[4].Tokens.Select(p => p.Value).ToList(), }; Assert.Equal("MyName", entity.Name); @@ -362,6 +401,9 @@ public async Task AetUpdate_Command() item => item.Equals("A"), item => item.Equals("B"), item => item.Equals("C")); + Assert.Collection(entity.PluginAssemblies, + item => item.Equals("PlugInAssemblyA"), + item => item.Equals("PlugInAssemblyB")); _informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Update(It.IsAny(), It.IsAny())) .ReturnsAsync(entity);