From 0ba5ba56886aed1dfaea7d01caa49f2d943ef0d9 Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Fri, 1 Sep 2023 16:19:15 -0700 Subject: [PATCH] Release/0.4.0 (#458) +semver: minor * gh-418 Implement data input plug-in engine and integration with SCP service * gh-419 Implement data output plug-in engine and integration with SCU/DICOMWeb export services * gh-435 Fix CLI to read log dir path from NLog config file * gh-434 New APIs to get a list of input/output data plug-ins * Virtual AE & Dynamic DICOMWeb STOW-RS Endpoints (#448) * Support new Workflow Request event data structure (#455) * Handle exception thrown from plug-in engine * Copyedit doc changes for the 0.4.0 release (#464) --------- Signed-off-by: Victor Chang Co-authored-by: Neil South Co-authored-by: Neil South <104848880+neildsouth@users.noreply.github.com> Co-authored-by: Coco-Ben <74689073+Coco-Ben@users.noreply.github.com> --- .dockerignore | 7 +- .editorconfig | 8 +- .github/workflows/ci.yml | 10 +- .licenserc.yaml | 1 + .trivyignore | 19 + doc/dependency_decisions.yml | 236 ++- docs/api/rest/config.md | 98 +- docs/api/rest/dicomweb-stow.md | 100 +- docs/changelog.md | 43 +- docs/compliance/third-party-licenses.md | 79 +- docs/docfx.json | 18 +- docs/index.md | 57 +- docs/plug-ins/overview.md | 146 ++ docs/plug-ins/remote-app.md | 61 + docs/plug-ins/toc.yml | 18 + docs/setup/cli.md | 9 +- docs/setup/schema.md | 170 +- docs/setup/services.md | 120 ++ docs/setup/setup.md | 92 +- docs/setup/toc.md | 1 + docs/toc.yml | 2 + guidelines/diagrams/mig-export.png | Bin 162667 -> 218257 bytes guidelines/diagrams/mig-export.txt | 14 +- guidelines/diagrams/mig-scp.png | Bin 196026 -> 265236 bytes guidelines/diagrams/mig-scp.txt | 14 +- guidelines/diagrams/mig.png | Bin 193273 -> 205762 bytes guidelines/{srs.md => mig-sadd.md} | 90 +- guidelines/{mig-drd.md => mig-srs.md} | 21 +- .../ExportRequestDataMessage.cs | 27 +- src/Api/IsExternalinit.cs | 2 +- ...Monai.Deploy.InformaticsGateway.Api.csproj | 9 +- src/Api/MonaiApplicationEntity.cs | 8 + src/Api/PlugIns/IInputDataPlugin.cs | 34 + src/Api/PlugIns/IInputDataPluginEngine.cs | 42 + src/Api/PlugIns/IOutputDataPlugin.cs | 33 + src/Api/PlugIns/IOutputDataPluginEngine.cs | 38 + src/Api/PlugIns/PluginNameAttribute.cs | 34 + src/Api/PlugIns/SR.cs | 27 + src/Api/Rest/InferenceRequest.cs | 14 +- src/Api/Storage/DicomFileStorageMetadata.cs | 36 +- src/Api/Storage/FhirFileStorageMetadata.cs | 29 +- src/Api/Storage/FileStorageMetadata.cs | 63 +- src/Api/Storage/Hl7FileStorageMetadata.cs | 13 +- src/Api/Storage/Payload.cs | 35 +- src/Api/Storage/StorageObjectMetadata.cs | 10 +- ....Deploy.InformaticsGateway.Api.Test.csproj | 10 +- .../Storage/DicomFileStorageMetadataTest.cs | 34 +- .../Storage/FhirFileStorageMetadataTest.cs | 15 +- src/Api/Test/Storage/PayloadTest.cs | 59 +- src/Api/Test/packages.lock.json | 250 ++- src/Api/VirtualApplicationEntity.cs | 118 ++ src/Api/packages.lock.json | 179 +- src/AssemblyInfo.cs | 1 - src/CLI/Commands/AetCommand.cs | 107 +- src/CLI/Commands/CommandBase.cs | 10 +- src/CLI/Commands/ConfigCommand.cs | 8 +- src/CLI/Commands/DestinationCommand.cs | 94 +- src/CLI/Commands/RestartCommand.cs | 2 +- src/CLI/Commands/SourceCommand.cs | 16 +- src/CLI/Commands/StartCommand.cs | 2 +- src/CLI/Commands/StatusCommand.cs | 2 +- src/CLI/Commands/StopCommand.cs | 2 +- src/CLI/ControlException.cs | 2 +- src/CLI/ExitCodes.cs | 4 +- src/CLI/Logging/ConsoleLogger.cs | 2 +- .../Logging/ConsoleLoggerFactoryExtensions.cs | 2 +- src/CLI/Logging/Log.cs | 15 +- ...Monai.Deploy.InformaticsGateway.CLI.csproj | 9 +- src/CLI/Options/Common.cs | 3 + src/CLI/Program.cs | 2 + src/CLI/Properties/launchSettings.json | 2 +- .../Services/ConfigurationOptionAccessor.cs | 24 +- src/CLI/Services/ConfigurationService.cs | 33 +- src/CLI/Services/ConfirmationPrompt.cs | 2 +- src/CLI/Services/DockerRunner.cs | 19 +- src/CLI/Services/EmbeddedResource.cs | 5 +- src/CLI/Services/IConfigurationService.cs | 5 + .../NLogConfigurationOptionAccessor.cs | 60 + src/CLI/Test/AetCommandTest.cs | 116 +- .../Test/ConfigurationOptionAccessorTest.cs | 192 ++ src/CLI/Test/ConfigurationServiceTest.cs | 40 +- src/CLI/Test/DestinationCommandTest.cs | 72 +- src/CLI/Test/DockerRunnerTest.cs | 2 +- ....Deploy.InformaticsGateway.CLI.Test.csproj | 12 +- .../NLogConfigurationOptionAccessorTest.cs | 55 + src/CLI/Test/packages.lock.json | 287 ++- src/CLI/packages.lock.json | 1159 ++---------- ...oy.InformaticsGateway.Client.Common.csproj | 3 +- src/Client.Common/ProblemException.cs | 2 +- ...formaticsGateway.Client.Common.Test.csproj | 14 +- src/Client.Common/Test/packages.lock.json | 125 +- src/Client.Common/packages.lock.json | 37 +- src/Client/HttpContentExtensions.cs | 34 + src/Client/IInformaticsGatewayClient.cs | 7 +- src/Client/InformaticsGatewayClient.cs | 6 +- ...ai.Deploy.InformaticsGateway.Client.csproj | 5 - src/Client/Services/AeTitle{T}Service.cs | 34 +- src/Client/Services/HealthService.cs | 2 +- src/Client/Test/AeTitleServiceTest.cs | 59 + ...ploy.InformaticsGateway.Client.Test.csproj | 12 +- src/Client/Test/packages.lock.json | 1056 ++++++----- src/Client/packages.lock.json | 1059 +---------- ...ai.Deploy.InformaticsGateway.Common.csproj | 4 +- ...ploy.InformaticsGateway.Common.Test.csproj | 12 +- src/Common/Test/packages.lock.json | 220 +-- src/Common/packages.lock.json | 127 +- src/Configuration/DicomWebConfiguration.cs | 18 +- .../InformaticsGatewayConfiguration.cs | 9 +- ...oy.InformaticsGateway.Configuration.csproj | 8 - src/Configuration/PluginConfiguration.cs | 27 + src/Configuration/ScuConfiguration.cs | 6 +- ...formaticsGateway.Configuration.Test.csproj | 12 +- src/Configuration/Test/packages.lock.json | 263 ++- src/Configuration/ValidationExtensions.cs | 29 +- src/Configuration/packages.lock.json | 228 +-- .../DatabaseOptions.cs} | 6 +- src/Database/Api/DatabaseRegistrationBase.cs | 25 + src/Database/Api/DatabaseTypes.cs | 24 + src/Database/Api/IDatabaseMigrationManager.cs | 11 + ...loy.InformaticsGateway.Database.Api.csproj | 5 - .../IVirtualApplicationEntityRepository.cs | 38 + .../InferenceRequestRepositoryBase.cs | 6 +- .../StorageMetadataRepositoryBase.cs | 10 +- src/Database/Api/SR.cs | 2 +- src/Database/Api/StorageMetadataWrapper.cs | 18 +- ...nformaticsGateway.Database.Api.Test.csproj | 10 +- .../Api/Test/StorageMetadataWrapperTest.cs | 34 +- src/Database/Api/Test/packages.lock.json | 313 ++-- src/Database/Api/packages.lock.json | 230 +-- src/Database/DatabaseManager.cs | 79 +- src/Database/DatabaseMigrationManager.cs | 44 +- .../InferenceRequestConfiguration.cs | 2 + .../MonaiApplicationEntityConfiguration.cs | 9 +- .../Configuration/PayloadConfiguration.cs | 19 +- .../VirtualApplicationEntityConfiguration.cs | 65 + .../InformaticsGatewayContext.cs | 2 + .../Migrations/20230327190827_R3_0.3.15.cs | 3 +- .../20230824185313_R4_0.4.0.Designer.cs | 378 ++++ .../Migrations/20230824185313_R4_0.4.0.cs | 96 + .../InformaticsGatewayContextModelSnapshot.cs | 58 +- ...icsGateway.Database.EntityFramework.csproj | 8 +- .../DestinationApplicationEntityRepository.cs | 12 +- .../DicomAssociationInfoRepository.cs | 6 +- .../InferenceRequestRepository.cs | 10 +- .../MonaiApplicationEntityRepository.cs | 12 +- .../Repositories/PayloadRepository.cs | 10 +- .../SourceApplicationEntityRepository.cs | 14 +- .../StorageMetadataWrapperRepository.cs | 16 +- .../VirtualApplicationEntityRepository.cs | 153 ++ ...tinationApplicationEntityRepositoryTest.cs | 2 +- ...teway.Database.EntityFramework.Test.csproj | 14 +- .../MonaiApplicationEntityRepositoryTest.cs | 7 +- .../Test/PayloadRepositoryTest.cs | 64 +- .../Test/SqliteDatabaseFixture.cs | 21 +- .../StorageMetadataWrapperRepositoryTest.cs | 30 +- .../VirtualApplicationEntityRepositoryTest.cs | 171 ++ .../EntityFramework/Test/packages.lock.json | 303 ++-- .../EntityFramework/packages.lock.json | 236 ++- ....Deploy.InformaticsGateway.Database.csproj | 6 +- .../Configurations/PayloadConfiguration.cs | 6 +- .../VirtualApplicationEntityConfiguration.cs | 33 + ...tinationApplicationEntityRepositoryTest.cs | 1 - .../DicomAssociationInfoRepositoryTest.cs | 1 - .../InferenceRequestRepositoryTest.cs | 1 - ...y.Database.MongoDB.Integration.Test.csproj | 14 +- .../MonaiApplicationEntityRepositoryTest.cs | 1 - .../Integration.Test/MongoDatabaseFixture.cs | 25 +- .../Integration.Test/PayloadRepositoryTest.cs | 66 +- .../SourceApplicationEntityRepositoryTest.cs | 3 - .../StorageMetadataWrapperRepositoryTest.cs | 31 +- .../VirtualApplicationEntityRepositoryTest.cs | 177 ++ .../Integration.Test/packages.lock.json | 356 ++-- ...InformaticsGateway.Database.MongoDB.csproj | 4 +- .../MongoDB/MongoDatabaseMigrationManager.cs | 3 +- .../DestinationApplicationEntityRepository.cs | 19 +- .../DicomAssociationInfoRepository.cs | 14 +- .../InferenceRequestRepository.cs | 17 +- .../MonaiApplicationEntityRepository.cs | 19 +- .../MongoDB/Repositories/PayloadRepository.cs | 18 +- .../SourceApplicationEntityRepository.cs | 19 +- .../StorageMetadataWrapperRepository.cs | 22 +- .../VirtualApplicationEntityRepository.cs | 175 ++ src/Database/MongoDB/packages.lock.json | 288 ++- src/Database/packages.lock.json | 338 ++-- ...ormaticsGateway.DicomWeb.Client.CLI.csproj | 2 - src/DicomWebClient/CLI/Qido.cs | 8 +- src/DicomWebClient/CLI/Stow.cs | 2 +- src/DicomWebClient/CLI/Utils.cs | 34 +- src/DicomWebClient/CLI/Wado.cs | 16 +- src/DicomWebClient/CLI/packages.lock.json | 99 +- .../Common/HttpResponseMessageExtension.cs | 8 +- src/DicomWebClient/DicomWebClient.cs | 6 +- ....InformaticsGateway.DicomWeb.Client.csproj | 9 +- src/DicomWebClient/Services/QidoService.cs | 6 +- src/DicomWebClient/Services/ServiceBase.cs | 2 +- src/DicomWebClient/Services/StowService.cs | 2 +- src/DicomWebClient/Services/WadoService.cs | 32 +- ...rmaticsGateway.DicomWeb.Client.Test.csproj | 14 +- src/DicomWebClient/Test/packages.lock.json | 177 +- src/DicomWebClient/packages.lock.json | 93 +- .../ApplicationEntityNotFoundException.cs | 40 + .../Common/DicomExtensions.cs | 2 +- src/InformaticsGateway/Common/DicomToolkit.cs | 6 +- .../Common/FileStorageMetadataExtensions.cs | 14 +- .../Common/IDicomToolkit.cs | 0 .../Common/PlugInLoadingException.cs | 31 + .../Common/TypeExtensions.cs | 83 + .../Logging/FoDicomLogManager.cs | 40 - .../Logging/Log.500.ExportService.cs | 3 + .../Logging/Log.5000.DataPlugins.cs | 41 + .../Logging/Log.8000.HttpServices.cs | 24 + .../Logging/LoggingExtensions.cs | 34 - .../Logging/MicrosoftLoggerAdapter.cs | 41 - ... - Backup.Deploy.InformaticsGateway.csproj | 114 ++ .../Monai.Deploy.InformaticsGateway.csproj | 31 +- src/InformaticsGateway/Program.cs | 11 +- .../Repositories/MonaiServiceLocator.cs | 2 +- .../Common/IInputDataPluginEngineFactory.cs | 122 ++ .../Services/Common/ITcpListener.cs | 2 +- .../Services/Common/InputDataPluginEngine.cs | 88 + .../Services/Common/OutputDataPluginEngine.cs | 94 + .../Connectors/DataRetrievalService.cs | 93 +- .../Services/Connectors/IPayloadAssembler.cs | 15 +- .../Services/Connectors/PayloadAssembler.cs | 26 +- .../Connectors/PayloadMoveActionHandler.cs | 14 +- .../PayloadNotificationActionHandler.cs | 20 +- .../Connectors/PayloadNotificationService.cs | 11 +- .../DicomWeb/DicomInstanceReaderBase.cs | 6 +- .../Services/DicomWeb/IStowService.cs | 4 +- .../Services/DicomWeb/IStreamsWriter.cs | 83 +- .../DicomWeb/MultipartDicomInstanceReader.cs | 4 +- .../DicomWeb/SingleDicomInstanceReader.cs | 4 +- .../Services/DicomWeb/StowService.cs | 33 +- .../Services/Export/DicomWebExportService.cs | 8 +- .../Export/ExportRequestEventDetails.cs | 4 +- .../Services/Export/ExportServiceBase.cs | 35 +- .../Services/Export/ScuExportService.cs | 2 +- .../Services/Fhir/FhirJsonReader.cs | 15 +- .../Fhir/FhirResourceTypesRouteConstraint.cs | 8 +- .../Services/Fhir/FhirService.cs | 14 +- .../Services/Fhir/FhirXmlReader.cs | 17 +- .../Services/HealthLevel7/IMllpClient.cs | 6 +- .../Services/HealthLevel7/MllpClient.cs | 19 +- .../Services/HealthLevel7/MllpService.cs | 13 +- .../Http/DestinationAeTitleController.cs | 26 +- .../Services/Http/DicomWeb/StowController.cs | 24 +- .../Services/Http/InferenceController.cs | 4 +- .../Services/Http/MonaiAeTitleController.cs | 27 +- .../Services/Http/VirtualAeTitleController.cs | 249 +++ .../Services/Scp/ApplicationEntityHandler.cs | 41 +- .../Services/Scp/ApplicationEntityManager.cs | 13 +- .../Services/Scp/IApplicationEntityManager.cs | 6 - .../Scp/MonaiAeChangedNotificationService.cs | 4 +- .../Services/Scp/ScpService.cs | 17 +- .../Services/Scp/ScpServiceInternal.cs | 10 +- .../Services/Scu/ScuQueue.cs | 2 +- .../Services/Scu/ScuService.cs | 11 +- .../Services/Scu/ScuWorkRequest.cs | 6 +- .../Services/Storage/ObjectUploadQueue.cs | 2 +- .../Services/Storage/ObjectUploadService.cs | 31 +- .../DicomFileStorageMetadataExtensionsTest.cs | 25 +- ...onai.Deploy.InformaticsGateway.Test.csproj | 30 +- ...loy.InformaticsGateway.Test.PlugIns.csproj | 31 + .../Test/Plug-ins/TestInputDataPlugIns.cs | 83 + .../Test/Plug-ins/TestOutputDataPlugIns.cs | 52 + src/InformaticsGateway/Test/ProgramTest.cs | 13 +- .../InputDataPluginEngineFactoryTest.cs | 70 + .../Common/InputDataPluginEngineTest.cs | 155 ++ .../OutputDataPluginEngineFactoryTest.cs | 67 + .../Common/OutputDataPluginEngineTest.cs | 152 ++ .../Connectors/DataRetrievalServiceTest.cs | 35 +- .../Connectors/PayloadAssemblerTest.cs | 42 +- .../PayloadMoveActionHandlerTest.cs | 30 +- .../PayloadNotificationActionHandlerTest.cs | 29 +- .../PayloadNotificationServiceTest.cs | 38 +- .../Test/Services/DicomWeb/StowServiceTest.cs | 119 +- .../Services/DicomWeb/StreamsWriterTest.cs | 193 +- .../Export/DicomWebExportServiceTest.cs | 40 +- .../Services/Export/ExportServiceBaseTest.cs | 36 +- .../Services/Export/ScuExportServiceTest.cs | 39 +- .../Test/Services/Fhir/FhirJsonReaderTest.cs | 25 +- .../Test/Services/Fhir/FhirServiceTest.cs | 13 +- .../Test/Services/Fhir/FhirXmlReaderTest.cs | 18 +- .../Services/HealthLevel7/MllpServiceTest.cs | 9 +- .../Http/DestinationAeTitleControllerTest.cs | 48 +- .../Http/DicomWeb/StowControllerTest.cs | 12 +- .../Http/MonaiAeTitleControllerTest.cs | 49 +- .../Http/VirtualAeTitleControllerTest.cs | 527 ++++++ .../Scp/ApplicationEntityHandlerTest.cs | 24 +- .../Test/Services/Scp/ScpServiceTest.cs | 8 +- .../Storage/ObjectUploadServiceTest.cs | 39 +- .../Test/Shared/DicomScpFixture.cs | 4 +- src/InformaticsGateway/Test/appsettings.json | 2 +- .../Test/packages.lock.json | 1059 ++++++----- .../appsettings.Development.json | 14 +- src/InformaticsGateway/appsettings.json | 18 +- src/InformaticsGateway/packages.lock.json | 1007 ++++++----- src/Monai.Deploy.InformaticsGateway.sln | 46 +- .../Database/DatabaseRegistrar.cs | 50 + .../EntityFramework/MigrationManager.cs | 48 + .../RemoteAppExecutionConfiguration.cs | 65 + .../RemoteAppExecutionDbContext.cs | 70 + .../RemoteAppExecutionDbContextFactory.cs | 44 + .../RemoteAppExecutionRepository.cs | 139 ++ .../Database/IRemoteAppExecutionRepository.cs | 29 + .../Database/MongoDb/MigrationManager.cs | 30 + .../RemoteAppExecutionConfiguration.cs | 35 + .../MongoDb/RemoteAppExecutionRepository.cs | 167 ++ .../RemoteAppExecution/DicomDeidentifier.cs | 101 ++ .../RemoteAppExecution/DicomReidentifier.cs | 69 + .../RemoteAppExecution/InternalVisibleTo.cs | 19 + .../Log.10000.DataPlugins.cs | 29 + .../20230818161328_R4_0.4.0.Designer.cs | 72 + .../Migrations/20230818161328_R4_0.4.0.cs | 52 + ...emoteAppExecutionDbContextModelSnapshot.cs | 70 + ...sGateway.PlugIns.RemoteAppExecution.csproj | 64 + .../RemoteAppExecution/RemoteAppExecution.cs | 85 + src/Plug-ins/RemoteAppExecution/SR.cs | 23 + .../Test/Database/DatabaseRegistrarTest.cs | 66 + .../EntityFramework/MigrationManagerTest.cs | 57 + .../RemoteAppExecutionRepositoryTest.cs | 169 ++ .../EntityFramework/SqliteDatabaseFixture.cs | 87 + .../Database/MongoDb/MongoDatabaseFixture.cs | 90 + .../RemoteAppExecutionRepositoryTest.cs | 173 ++ .../Test/DicomDeidentifierTest.cs | 262 +++ .../Test/DicomReidentifierTest.cs | 134 ++ ...way.PlugIns.RemoteAppExecution.Test.csproj | 64 + .../Test/packages.lock.json | 1601 +++++++++++++++++ src/Plug-ins/RemoteAppExecution/Utilities.cs | 59 + .../RemoteAppExecution/appsettings.json | 5 + .../RemoteAppExecution/packages.lock.json | 577 ++++++ src/Shared/Test/InstanceGenerator.cs | 5 +- src/Shared/Test/TestStorageInfo.cs | 10 +- tests/Integration.Test/Common/Assertions.cs | 131 +- tests/Integration.Test/Common/DataProvider.cs | 147 +- .../Common/DicomCEchoDataClient.cs | 2 +- .../Common/DicomCStoreDataClient.cs | 6 +- .../Integration.Test/Common/DicomDataSpecs.cs | 2 +- tests/Integration.Test/Common/DicomScp.cs | 34 +- .../Common/DicomWebDataSink.cs | 4 +- tests/Integration.Test/Common/FhirDataSink.cs | 2 +- tests/Integration.Test/Common/Hl7DataSink.cs | 4 +- .../Integration.Test/Common/MinioDataSink.cs | 2 +- .../Common/RabbitConnectionFactory.cs | 6 +- .../Drivers/DicomInstanceGenerator.cs | 4 +- .../Drivers/EfDataProvider.cs | 5 +- .../Drivers/MongoDBDataProvider.cs | 5 +- .../Drivers/RabbitMqConsumer.cs | 3 +- .../Features/DicomDimseScp.feature | 2 +- .../Features/DicomWebStow.feature | 42 +- .../Features/RemoteAppExecutionPlugIn.feature | 27 + ...InformaticsGateway.Integration.Test.csproj | 38 +- .../DicomDimseScpServicesStepDefinitions.cs | 50 +- .../DicomWebStowServiceStepDefinitions.cs | 97 +- .../ExportServicesStepDefinitions.cs | 40 +- .../StepDefinitions/FhirDefinitions.cs | 7 +- .../HealthLevel7Definitions.cs | 2 +- ...emoteAppExecutionPlugInsStepDefinitions.cs | 240 +++ .../StepDefinitions/SharedDefinitions.cs | 30 +- tests/Integration.Test/appsettings.json | 6 + tests/Integration.Test/packages.lock.json | 1027 ++++++----- 361 files changed, 17358 insertions(+), 8260 deletions(-) mode change 100644 => 100755 .dockerignore create mode 100644 .trivyignore create mode 100644 docs/plug-ins/overview.md create mode 100644 docs/plug-ins/remote-app.md create mode 100644 docs/plug-ins/toc.yml create mode 100644 docs/setup/services.md rename guidelines/{srs.md => mig-sadd.md} (77%) rename guidelines/{mig-drd.md => mig-srs.md} (95%) rename src/{InformaticsGateway/Services/Export => Api}/ExportRequestDataMessage.cs (72%) mode change 100644 => 100755 create mode 100644 src/Api/PlugIns/IInputDataPlugin.cs create mode 100644 src/Api/PlugIns/IInputDataPluginEngine.cs create mode 100644 src/Api/PlugIns/IOutputDataPlugin.cs create mode 100644 src/Api/PlugIns/IOutputDataPluginEngine.cs create mode 100644 src/Api/PlugIns/PluginNameAttribute.cs create mode 100644 src/Api/PlugIns/SR.cs create mode 100644 src/Api/VirtualApplicationEntity.cs create mode 100644 src/CLI/Services/NLogConfigurationOptionAccessor.cs create mode 100644 src/CLI/Test/ConfigurationOptionAccessorTest.cs create mode 100644 src/CLI/Test/NLogConfigurationOptionAccessorTest.cs create mode 100644 src/Client/HttpContentExtensions.cs create mode 100755 src/Configuration/PluginConfiguration.cs rename src/Database/{MongoDB/Configurations/MongoDBOptions.cs => Api/DatabaseOptions.cs} (80%) create mode 100644 src/Database/Api/DatabaseRegistrationBase.cs create mode 100644 src/Database/Api/DatabaseTypes.cs create mode 100644 src/Database/Api/Repositories/IVirtualApplicationEntityRepository.cs mode change 100644 => 100755 src/Database/DatabaseManager.cs create mode 100644 src/Database/EntityFramework/Configuration/VirtualApplicationEntityConfiguration.cs mode change 100644 => 100755 src/Database/EntityFramework/InformaticsGatewayContext.cs create mode 100644 src/Database/EntityFramework/Migrations/20230824185313_R4_0.4.0.Designer.cs create mode 100644 src/Database/EntityFramework/Migrations/20230824185313_R4_0.4.0.cs create mode 100644 src/Database/EntityFramework/Repositories/VirtualApplicationEntityRepository.cs create mode 100644 src/Database/EntityFramework/Test/VirtualApplicationEntityRepositoryTest.cs create mode 100644 src/Database/MongoDB/Configurations/VirtualApplicationEntityConfiguration.cs mode change 100644 => 100755 src/Database/MongoDB/Integration.Test/MongoDatabaseFixture.cs create mode 100644 src/Database/MongoDB/Integration.Test/VirtualApplicationEntityRepositoryTest.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/DestinationApplicationEntityRepository.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/DicomAssociationInfoRepository.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/InferenceRequestRepository.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/MonaiApplicationEntityRepository.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/PayloadRepository.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/SourceApplicationEntityRepository.cs mode change 100644 => 100755 src/Database/MongoDB/Repositories/StorageMetadataWrapperRepository.cs create mode 100644 src/Database/MongoDB/Repositories/VirtualApplicationEntityRepository.cs create mode 100644 src/InformaticsGateway/Common/ApplicationEntityNotFoundException.cs mode change 100644 => 100755 src/InformaticsGateway/Common/IDicomToolkit.cs create mode 100644 src/InformaticsGateway/Common/PlugInLoadingException.cs create mode 100644 src/InformaticsGateway/Common/TypeExtensions.cs delete mode 100644 src/InformaticsGateway/Logging/FoDicomLogManager.cs create mode 100644 src/InformaticsGateway/Logging/Log.5000.DataPlugins.cs delete mode 100644 src/InformaticsGateway/Logging/LoggingExtensions.cs delete mode 100644 src/InformaticsGateway/Logging/MicrosoftLoggerAdapter.cs create mode 100644 src/InformaticsGateway/Monai - Backup.Deploy.InformaticsGateway.csproj mode change 100644 => 100755 src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj mode change 100644 => 100755 src/InformaticsGateway/Program.cs create mode 100644 src/InformaticsGateway/Services/Common/IInputDataPluginEngineFactory.cs create mode 100644 src/InformaticsGateway/Services/Common/InputDataPluginEngine.cs create mode 100644 src/InformaticsGateway/Services/Common/OutputDataPluginEngine.cs mode change 100644 => 100755 src/InformaticsGateway/Services/Connectors/PayloadAssembler.cs mode change 100644 => 100755 src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs create mode 100644 src/InformaticsGateway/Services/Http/VirtualAeTitleController.cs create mode 100644 src/InformaticsGateway/Test/Plug-ins/Monai.Deploy.InformaticsGateway.Test.PlugIns.csproj create mode 100644 src/InformaticsGateway/Test/Plug-ins/TestInputDataPlugIns.cs create mode 100644 src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs create mode 100644 src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineFactoryTest.cs create mode 100644 src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs create mode 100644 src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineFactoryTest.cs create mode 100644 src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs create mode 100644 src/InformaticsGateway/Test/Services/Http/VirtualAeTitleControllerTest.cs mode change 100644 => 100755 src/InformaticsGateway/appsettings.json create mode 100644 src/Plug-ins/RemoteAppExecution/Database/DatabaseRegistrar.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/MigrationManager.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionConfiguration.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContext.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContextFactory.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionRepository.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/IRemoteAppExecutionRepository.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/MongoDb/MigrationManager.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionConfiguration.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionRepository.cs create mode 100644 src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs create mode 100644 src/Plug-ins/RemoteAppExecution/DicomReidentifier.cs create mode 100644 src/Plug-ins/RemoteAppExecution/InternalVisibleTo.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Log.10000.DataPlugins.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.Designer.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Migrations/RemoteAppExecutionDbContextModelSnapshot.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj create mode 100644 src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs create mode 100644 src/Plug-ins/RemoteAppExecution/SR.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/DicomReidentifierTest.cs create mode 100644 src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj create mode 100644 src/Plug-ins/RemoteAppExecution/Test/packages.lock.json create mode 100644 src/Plug-ins/RemoteAppExecution/Utilities.cs create mode 100644 src/Plug-ins/RemoteAppExecution/appsettings.json create mode 100644 src/Plug-ins/RemoteAppExecution/packages.lock.json create mode 100644 tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature create mode 100644 tests/Integration.Test/StepDefinitions/RemoteAppExecutionPlugInsStepDefinitions.cs diff --git a/.dockerignore b/.dockerignore old mode 100644 new mode 100755 index 614b191c3..306cb0540 --- a/.dockerignore +++ b/.dockerignore @@ -73,4 +73,9 @@ bld/ # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!*.[Cc]ache/ \ No newline at end of file +!*.[Cc]ache/ + +docker-compose/ +doc +guidlines +tests diff --git a/.editorconfig b/.editorconfig index 35a26b081..7e85a00c9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# Copyright 2022 MONAI Consortium +# Copyright 2022-2023 MONAI Consortium # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ root = true # Copyright File Header -file_header_template = SPDX-FileCopyrightText: © [year file created] - [last year file modified], MONAI Consortium\nSPDX-License-Identifier: Apache License 2.0 +file_header_template = SPDX-FileCopyrightText: © [year file created] - [last year file modified], MONAI Consortium\nSPDX-License-Identifier: Apache License 2.0 dotnet_diagnostic.IDE0073.severity = error # Default settings: @@ -309,3 +309,7 @@ max_line_length = 88 [*.json] indent_size = 2 insert_final_newline = ignore + + +# Spelling +spelling_error_severity = information diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9127e26aa..875e527a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -255,7 +255,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - feature: [AcrApi, DicomDimseScp, DicomDimseScu, DicomWebExport, DicomWebStow, HealthLevel7, Fhir] + feature: [AcrApi, DicomDimseScp, DicomDimseScu, DicomWebExport, DicomWebStow, HealthLevel7, Fhir, RemoteAppExecutionPlugIn] database: [ef, mongodb] fail-fast: false env: @@ -389,7 +389,7 @@ jobs: retention-days: 7 - name: Log in to the Container registry - uses: docker/login-action@v2.1.0 + uses: docker/login-action@v2.2.0 if: ${{ (matrix.os == 'ubuntu-latest') }} with: registry: ${{ env.REGISTRY }} @@ -398,7 +398,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4.3.0 + uses: docker/metadata-action@v4.6.0 if: ${{ (matrix.os == 'ubuntu-latest') }} with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -407,7 +407,7 @@ jobs: type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - name: Build and push Docker image - uses: docker/build-push-action@v4.0.0 + uses: docker/build-push-action@v4.1.1 if: ${{ (matrix.os == 'ubuntu-latest') }} with: context: . @@ -425,7 +425,7 @@ jobs: - name: Anchore container scan id: anchore-scan - uses: anchore/scan-action@v3.3.5 + uses: anchore/scan-action@v3.3.6 if: ${{ (matrix.os == 'ubuntu-latest') }} with: image: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} diff --git a/.licenserc.yaml b/.licenserc.yaml index f7d23c3d8..b1ea23d84 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -31,6 +31,7 @@ header: - 'src/coverlet.runsettings' - 'src/Monai.Deploy.InformaticsGateway.sln' - 'src/Database/EntityFramework/Migrations/**' + - 'src/Plug-ins/RemoteAppExecution/Migrations/**' - 'demos/**/.env/**' - 'demos/**/*.txt' - 'doc/dependency_decisions.yml' diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 000000000..3626c2622 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,19 @@ +# Copyright 2023 MONAI Consortium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://github.com/Project-MONAI/monai-deploy-informatics-gateway/issues/450 +CVE-2018-8292 + +# https://github.com/Project-MONAI/monai-deploy-informatics-gateway/issues/451 +CVE-2019-0820 diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index c0d442e64..5e1a5ecfe 100644 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -4,8 +4,7 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/aws/aws-sdk-net/raw/master/License.txt) :versions: - - 3.7.100.25 - - 3.7.105.20 + - 3.7.200.13 :when: 2022-09-01 23:05:22.203089302 Z - - :approve - AWSSDK.S3 @@ -19,8 +18,7 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/aws/aws-sdk-net/raw/master/License.txt) :versions: - - 3.7.100.25 - - 3.7.101.26 + - 3.7.201.9 :when: 2022-09-01 23:05:28.920368903 Z - - :approve - BoDi @@ -69,7 +67,7 @@ - :who: mocsharp :why: MIT (https://github.com/dotnet/Docker.DotNet/raw/master/LICENSE) :versions: - - 3.125.13 + - 3.125.15 :when: 2022-08-16 23:05:32.422217566 Z - - :approve - DotNext @@ -90,7 +88,7 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/fluentassertions/fluentassertions/raw/develop/LICENSE) :versions: - - 6.10.0 + - 6.11.0 :when: 2022-08-16 23:05:33.753437127 Z - - :approve - Gherkin @@ -111,7 +109,7 @@ - :who: mocsharp :why: MIT (https://github.com/Efferent-Health/HL7-dotnetcore/raw/master/LICENSE.txt) :versions: - - 2.35.0 + - 2.36.0 :when: 2022-08-16 23:05:35.066879864 Z - - :approve - Humanizer.Core @@ -295,13 +293,6 @@ :versions: - 6.0.0 :when: 2022-08-16 23:05:46.589538298 Z -- - :approve - - Microsoft.CSharp - - :who: mocsharp - :why: MICROSOFT .NET LIBRARY License ( http://go.microsoft.com/fwlink/?LinkId=329770) - :versions: - - 4.0.1 - :when: 2022-08-16 23:05:47.027783271 Z - - :approve - Microsoft.CSharp - :who: mocsharp @@ -314,6 +305,7 @@ - :who: mocsharp :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) :versions: + - 4.5.0 - 4.7.0 :when: 2022-08-16 23:05:47.910358457 Z - - :approve @@ -322,70 +314,70 @@ :why: MIT (https://github.com/microsoft/vstest/raw/main/LICENSE) :versions: - 17.4.1 - - 17.5.0 + - 17.7.1 :when: 2022-08-16 23:05:48.342748414 Z - - :approve - Microsoft.Data.Sqlite.Core - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:49.698463427 Z - - :approve - Microsoft.EntityFrameworkCore - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:50.137694970 Z - - :approve - Microsoft.EntityFrameworkCore.Abstractions - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:51.008105271 Z - - :approve - Microsoft.EntityFrameworkCore.Analyzers - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:51.445711308 Z - - :approve - Microsoft.EntityFrameworkCore.Design - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:51.922790944 Z - - :approve - Microsoft.EntityFrameworkCore.InMemory - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:52.375150938 Z - - :approve - Microsoft.EntityFrameworkCore.Relational - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:52.828879230 Z - - :approve - Microsoft.EntityFrameworkCore.Sqlite - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:53.270526921 Z - - :approve - Microsoft.EntityFrameworkCore.Sqlite.Core - :who: mocsharp :why: MIT (https://raw.githubusercontent.com/dotnet/efcore/release/6.0/LICENSE.txt) :versions: - - 6.0.15 + - 6.0.21 :when: 2022-08-16 23:05:53.706997823 Z - - :approve - Microsoft.Extensions.ApiDescription.Server @@ -526,24 +518,21 @@ - :who: mocsharp :why: MIT (https://github.com/dotnet/aspnetcore/raw/main/LICENSE.txt) :versions: - - 6.0.12 - - 6.0.15 + - 6.0.21 :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions - :who: mocsharp :why: MIT (https://github.com/dotnet/aspnetcore/raw/main/LICENSE.txt) :versions: - - 6.0.12 - - 6.0.15 + - 6.0.21 :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore - :who: mocsharp :why: MIT (https://github.com/dotnet/aspnetcore/raw/main/LICENSE.txt) :versions: - - 6.0.11 - - 6.0.15 + - 6.0.21 :when: 2022-08-29 18:11:22.090772006 Z - - :approve - Microsoft.Extensions.FileProviders.Abstractions @@ -607,8 +596,8 @@ :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) :versions: - 6.0.0 - - 6.0.2 - 6.0.3 + - 6.0.4 :when: 2022-08-16 23:06:06.728283354 Z - - :approve - Microsoft.Extensions.Logging.Configuration @@ -693,7 +682,7 @@ :why: MIT (https://raw.githubusercontent.com/microsoft/vstest/main/LICENSE) :versions: - 17.4.1 - - 17.5.0 + - 17.7.1 :when: 2022-09-01 23:06:13.008314524 Z - - :approve - Microsoft.NETCore.Platforms @@ -729,7 +718,7 @@ - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/aspnet/AspNetCore/2.0.0/LICENSE.txt) :versions: - - 2.2.8 + - 2.2.0 :when: 2022-08-16 23:06:15.238479186 Z - - :approve - Microsoft.OpenApi @@ -744,7 +733,7 @@ :why: MIT (https://github.com/microsoft/vstest/raw/v17.4.0/LICENSE) :versions: - 17.4.1 - - 17.5.0 + - 17.7.1 :when: 2022-08-16 23:06:16.175705981 Z - - :approve - Microsoft.TestPlatform.TestHost @@ -752,7 +741,7 @@ :why: MIT (https://github.com/microsoft/vstest/raw/v17.4.0/LICENSE) :versions: - 17.4.1 - - 17.5.0 + - 17.7.1 :when: 2022-08-16 23:06:17.671459450 Z - - :approve - Microsoft.Toolkit.HighPerformance @@ -787,43 +776,42 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/minio/minio-dotnet/raw/master/LICENSE) :versions: - - 4.0.6 - - 4.0.7 + - 5.0.0 :when: 2022-08-16 23:06:20.598551507 Z - - :approve - Monai.Deploy.Messaging - :who: neilsouth :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) :versions: - - 0.1.22 + - 1.0.0 :when: 2022-08-16 23:06:21.051573547 Z - - :approve - Monai.Deploy.Messaging.RabbitMQ - :who: neilsouth :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) :versions: - - 0.1.22 + - 1.0.0 :when: 2022-08-16 23:06:21.511789690 Z - - :approve - Monai.Deploy.Storage - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) :versions: - - 0.2.16 + - 0.2.18 :when: 2022-08-16 23:06:21.988183476 Z - - :approve - Monai.Deploy.Storage.MinIO - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) :versions: - - 0.2.16 + - 0.2.18 :when: 2022-08-16 23:06:22.426838304 Z - - :approve - Monai.Deploy.Storage.S3Policy - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) :versions: - - 0.2.16 + - 0.2.18 :when: 2022-08-16 23:06:22.881956546 Z - - :approve - Monai.Deploy.Security @@ -837,8 +825,7 @@ - :who: mocsharp :why: BSD 3-Clause License ( https://raw.githubusercontent.com/moq/moq4/main/License.txt) :versions: - - 4.18.1 - - 4.18.4 + - 4.20.69 :when: 2022-08-16 23:06:23.359197359 Z - - :approve - NETStandard.Library @@ -876,14 +863,14 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/NuGet/NuGet.Client/raw/dev/LICENSE.txt) :versions: - - 5.11.0 + - 6.5.0 :when: 2022-08-16 23:06:27.464713741 Z - - :approve - Polly - :who: mocsharp - :why: New BSD License (https://github.com/App-vNext/Polly/raw/main/LICENSE.txt) + :why: New BSD License (https://raw.githubusercontent.com/App-vNext/Polly/main/LICENSE) :versions: - - 7.2.3 + - 7.2.4 :when: 2022-08-16 23:06:27.913122244 Z - - :approve - Portable.BouncyCastle @@ -897,7 +884,7 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/rabbitmq/rabbitmq-dotnet-client/raw/main/LICENSE-APACHE2) :versions: - - 6.4.0 + - 6.5.0 :when: 2022-08-16 23:06:28.818109746 Z - - :approve - SQLitePCLRaw.bundle_e_sqlite3 @@ -1299,21 +1286,8 @@ :why: MIT (https://github.com/dotnet/corefx/raw/master/LICENSE.TXT) :versions: - 4.5.4 + - 4.5.5 :when: 2022-08-16 23:06:55.672403035 Z -- - :approve - - System.Net.Http - - :who: mocsharp - :why: MICROSOFT .NET LIBRARY License ( http://go.microsoft.com/fwlink/?LinkId=329770) - :versions: - - 4.3.0 - :when: 2022-08-16 23:06:56.146126058 Z -- - :approve - - System.Net.Http - - :who: mocsharp - :why: MICROSOFT .NET LIBRARY License ( http://go.microsoft.com/fwlink/?LinkId=329770) - :versions: - - 4.3.4 - :when: 2022-08-16 23:06:56.599420591 Z - - :approve - System.Net.NameResolution - :who: mocsharp @@ -1636,7 +1610,7 @@ - :who: mocsharp :why: MIT (https://github.com/dotnet/corefx/raw/master/LICENSE.TXT) :versions: - - 4.6.0 + - 6.0.0 :when: 2022-08-16 23:07:17.991171210 Z - - :approve - System.Text.Encoding.Extensions @@ -1659,20 +1633,13 @@ :versions: - 6.0.0 :when: 2022-08-16 23:07:19.377530263 Z -- - :approve - - System.Text.Json - - :who: mocsharp - :why: MIT (https://github.com/dotnet/corefx/raw/master/LICENSE.TXT) - :versions: - - 4.7.2 - :when: 2022-08-16 23:07:19.845361666 Z - - :approve - System.Text.Json - :who: mocsharp :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) :versions: - - 6.0.0 - 6.0.7 + - 6.0.8 :when: 2022-08-16 23:07:20.787263056 Z - - :approve - System.Text.RegularExpressions @@ -1701,6 +1668,7 @@ :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) :versions: - 6.0.0 + - 7.0.0 :when: 2022-08-16 23:07:22.653576384 Z - - :approve - System.Threading.Overlapped @@ -1798,14 +1766,14 @@ - :who: mocsharp :why: MIT (https://github.com/coverlet-coverage/coverlet/raw/master/LICENSE) :versions: - - 3.2.0 + - 6.0.0 :when: 2022-08-16 23:07:29.112978564 Z - - :approve - fo-dicom - :who: mocsharp :why: Microsoft Public License (https://github.com/fo-dicom/fo-dicom/raw/development/License.txt) :versions: - - 5.0.3 + - 5.1.1 :when: 2022-08-16 23:07:29.574869349 Z - - :approve - runtime.any.System.Collections @@ -2232,7 +2200,7 @@ - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) :versions: - - 2.4.1 + - 2.5.0 :when: 2022-08-16 23:07:58.264039741 Z - - :approve - xunit @@ -2253,112 +2221,70 @@ - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) :versions: - - 0.10.0 - :when: 2022-08-16 23:07:59.702393969 Z -- - :approve - - xunit.analyzers - - :who: mocsharp - :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) - :versions: - - 1.0.0 + - 1.2.0 :when: 2022-08-16 23:08:00.165216213 Z - - :approve - xunit.assert - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) :versions: - - 2.4.1 - :when: 2022-08-16 23:08:00.634240281 Z -- - :approve - - xunit.assert - - :who: mocsharp - :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) - :versions: - - 2.4.2 + - 2.5.0 :when: 2022-08-16 23:08:01.105384447 Z - - :approve - xunit.core - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) :versions: - - 2.4.1 - :when: 2022-08-16 23:08:01.570300282 Z -- - :approve - - xunit.core - - :who: mocsharp - :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) - :versions: - - 2.4.2 + - 2.5.0 :when: 2022-08-16 23:08:02.057792372 Z - - :approve - xunit.extensibility.core - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) :versions: - - 2.4.1 - :when: 2022-08-16 23:08:02.535203327 Z -- - :approve - - xunit.extensibility.core - - :who: mocsharp - :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) - :versions: - - 2.4.2 + - 2.5.0 :when: 2022-08-16 23:08:03.019024760 Z - - :approve - xunit.extensibility.execution - :who: mocsharp :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) :versions: - - 2.4.1 - :when: 2022-08-16 23:08:03.493713542 Z -- - :approve - - xunit.extensibility.execution - - :who: mocsharp - :why: Apache-2.0 ( https://raw.githubusercontent.com/xunit/xunit/master/license.txt) - :versions: - - 2.4.2 + - 2.5.0 :when: 2022-08-16 23:08:03.959558421 Z - - :approve - xunit.runner.visualstudio - :who: mocsharp :why: MIT ( https://licenses.nuget.org/MIT) :versions: - - 2.4.3 - :when: 2022-08-16 23:08:04.429394853 Z -- - :approve - - xunit.runner.visualstudio - - :who: mocsharp - :why: MIT ( https://licenses.nuget.org/MIT) - :versions: - - 2.4.5 + - 2.5.0 :when: 2022-08-16 23:08:04.892608686 Z - - :approve - Ardalis.GuardClauses - :who: mocsharp :why: MIT (https://github.com/ardalis/GuardClauses.Analyzers/raw/master/LICENSE) :versions: - - 4.0.1 + - 4.1.1 :when: 2022-08-16 23:10:21.184627612 Z - - :approve - NLog - :who: mocsharp :why: BSD 3-Clause License (https://github.com/NLog/NLog/raw/dev/LICENSE.txt) :versions: - - 5.1.3 + - 5.2.3 :when: 2022-10-12 03:14:06.538744982 Z - - :approve - NLog.Extensions.Logging - :who: mocsharp :why: BSD 2-Clause Simplified License (https://github.com/NLog/NLog.Extensions.Logging/raw/master/LICENSE) :versions: - - 5.2.3 + - 5.3.3 :when: 2022-10-12 03:14:06.964203977 Z - - :approve - NLog.Web.AspNetCore - :who: mocsharp :why: BSD 3-Clause License (https://github.com/NLog/NLog.Web/raw/master/LICENSE) :versions: - - 5.2.3 + - 5.3.3 :when: 2022-10-12 03:14:07.396706995 Z - - :approve - fo-dicom.NLog @@ -2398,30 +2324,30 @@ - - :approve - MongoDB.Bson - :who: mocsharp - :why: Apache-2.0 (https://github.com/mongodb/mongo-csharp-driver/raw/master/License.txt) + :why: Apache-2.0 (https://raw.githubusercontent.com/mongodb/mongo-csharp-driver/master/LICENSE.md) :versions: - - 2.19.1 + - 2.21.0 :when: 2022-11-16 23:38:53.891380809 Z - - :approve - MongoDB.Driver - :who: mocsharp - :why: Apache-2.0 (https://github.com/mongodb/mongo-csharp-driver/raw/master/License.txt) + :why: Apache-2.0 (https://raw.githubusercontent.com/mongodb/mongo-csharp-driver/master/LICENSE.md) :versions: - - 2.19.1 + - 2.21.0 :when: 2022-11-16 23:38:54.213853364 Z - - :approve - MongoDB.Driver.Core - :who: mocsharp - :why: Apache-2.0 (https://github.com/mongodb/mongo-csharp-driver/raw/master/License.txt) + :why: Apache-2.0 (https://raw.githubusercontent.com/mongodb/mongo-csharp-driver/master/LICENSE.md) :versions: - - 2.19.1 + - 2.21.0 :when: 2022-11-16 23:38:54.553730219 Z - - :approve - MongoDB.Libmongocrypt - :who: mocsharp - :why: Apache-2.0 (https://github.com/mongodb/mongo-csharp-driver/raw/master/License.txt) + :why: Apache-2.0 (https://raw.githubusercontent.com/mongodb/mongo-csharp-driver/master/LICENSE.md) :versions: - - 1.7.0 + - 1.8.0 :when: 2022-11-16 23:38:54.863359236 Z - - :approve - SharpCompress @@ -2493,4 +2419,46 @@ :versions: - 6.0.2 :when: 2022-12-08 23:37:56.206982078 Z +- - :approve + - CommunityToolkit.HighPerformance + - :who: mocsharp + :why: MIT (https://raw.githubusercontent.com/CommunityToolkit/dotnet/main/License.md) + :versions: + - 8.2.0 + :when: 2023-08-04 0:02:30.206982078 Z +- - :approve + - Microsoft.Bcl.HashCode + - :who: mocsharp + :why: MIT (https://licenses.nuget.org/MIT) + :versions: + - 1.1.1 + :when: 2023-08-04 0:02:30.206982078 Z +- - :approve + - Devlooped.SponsorLink + - :who: mocsharp + :why: MIT (https://licenses.nuget.org/MIT) + :versions: + - 1.0.0 + :when: 2023-08-08 0:08:05.206982078 Z +- - :approve + - System.IO.Hashing + - :who: mocsharp + :why: MIT (https://raw.githubusercontent.com/dotnet/runtime/main/LICENSE.TXT) + :versions: + - 7.0.0 + :when: 2023-08-10 20:50:14.759818552 Z +- - :approve + - System.Net.Http + - :who: mocsharp + :why: MICROSOFT .NET LIBRARY License ( http://go.microsoft.com/fwlink/?LinkId=329770) + :versions: + - 4.3.0 + - 4.3.4 + :when: 2022-08-16 23:07:41.735502132 Z + + + + + + diff --git a/docs/api/rest/config.md b/docs/api/rest/config.md index ec7735ac5..ad760d1cf 100644 --- a/docs/api/rest/config.md +++ b/docs/api/rest/config.md @@ -1,5 +1,5 @@ + +# Data Plug-ins + +Data plug-ins enable manipulation of incoming data before they are saved to the storage service or outgoing data right before they are exported. + +## Using Data Plug-ins + +The Informatics Gateway allows you to configure data plug-ins in the following services: + +- (DIMSE) MONAI Deploy DICOM Listener: Configure each listening AE Title with zero or more data plug-ins via the + [CLI](../setup/cli.md) or via the [Configuration API](../api/rest/config.md). +- (DIMSE) DICOM Export: configure the `Plug-inAssemblies` with one or more data plug-ins in the [ExportRequestEvent](https://github.com/Project-MONAI/monai-deploy-messaging/blob/main/src/Messaging/Events/ExportRequestEvent.cs#L85). +- (DICOMWeb) STOW-RS: + - The Virtual AE endpoints (`/dicomweb/vae/...`) can be configured similarly to the DICOM listener using the [DICOMWeb STOW API](../api/rest/dicomweb-stow.md##post-dicomwebvaeaetworkflow-idstudiesstudy-instance-uid). + - For the default `/dicomweb/...` endpoints, set zero or more plug-ins under `InformaticsGateway>dicomWeb>plug-ins` in the + `appsettings.json` [configuration](../setup/schema.md) file. +- (DICOMWeb) Export: configure the `Plug-inAssemblies` with one or more data plug-ins in the [ExportRequestEvent](https://github.com/Project-MONAI/monai-deploy-messaging/blob/main/src/Messaging/Events/ExportRequestEvent.cs#L85). + +> [!Note] +> When one or more plug-ins are defined, the plug-ins are executed in the order as they are listed. + +## Available Plug-ins + +The following plug-ins are available: + +| Name | Description | Fully Qualified Assembly Name | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| [DicomDeidentifier](./remote-app.md) | A plug-in that de-identifies a set of configurable DICOM tags with random data before DICOM data is exported. | `Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.DicomDeidentifier, Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution` | +| [DicomReidentifier](./remote-app.md) | A plug-in to be used together with the `DicomDeidentifier` plug-in to restore the original DICOM metadata. | `Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.DicomReidentifier, Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution` | + + +## Creating a Plug-in + +To create an input data plug-in, implement the [IInputDataPlugin](xref:Monai.Deploy.InformaticsGateway.Api.PlugIns.IInputDataPlugIn) interface and +put the [dynamic link library](https://learn.microsoft.com/en-us/troubleshoot/windows-client/deployment/dynamic-link-library) (DLL) in +the `plug-ins/` directories. Similarly, for output data plug-ins, implement the [IOutputDataPlugin](xref:Monai.Deploy.InformaticsGateway.Api.PlugIns.IOutputDataPlugIn) +interface. + +Refer to the [Configuration API](../api/rest/config.md) page to retrieve available [input](../api/rest/config.md#get-configaeplug-ins) and +[output](../api/rest/config.md#get-configdestinationplug-ins) data plug-ins. + + +### Database Extensions + +If a plug-in requires presistent data in the database, extend the [DatabaseRegistrationBase](xref:Monai.Deploy.InformaticsGateway.Database.Api.DatabaseRegistrationBase) +class to register your database context and repositories. + +Refer to the `Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution` plug-in as a reference. + +> [!Important] +> The Informatics Gateway requires all plug-ins to extend both the Entity Framework (SQLite) and MongoDB databases. + +#### Entity Framework + +Implement the [IDatabaseMigrationManagerForPlugIns](xref:Monai.Deploy.InformaticsGateway.Database.Api.IDatabaseMigrationManagerForPlugIns) interface to +register your Entity Framework (EF) database context. A `connectionString` is provided to the `Configure(...)` function when you extend the +[DatabaseRegistrationBase](xref:Monai.Deploy.InformaticsGateway.Database.Api.DatabaseRegistrationBase) class, allowing you to create your +[code-first](https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/workflows/new-database) EF database context and generate your +migration code using the [dotnet ef](https://learn.microsoft.com/en-us/ef/core/cli/dotnet) CLI tool. + +The following example extends the [DatabaseRegistrationBase](xref:Monai.Deploy.InformaticsGateway.Database.Api.DatabaseRegistrationBase) class +to register a new EF database context named `RemoteAppExecutionDbContext`. + +In the method, you first register the database context, then register your Migration Manager by implementing the +[IDatabaseMigrationManagerForPlugIns](xref:Monai.Deploy.InformaticsGateway.Database.Api.IDatabaseMigrationManagerForPlugIns) interface. +Lastly, you register your repository for the `RemoteAppExecutions` table. + +```csharp +public class DatabaseRegistrar : DatabaseRegistrationBase +{ + public override IServiceCollection Configure(IServiceCollection services, DatabaseType databaseType, string? connectionString) + { + Guard.Against.Null(services, nameof(services)); + + switch (databaseType) + { + case DatabaseType.EntityFramework: + Guard.Against.Null(connectionString, nameof(connectionString)); + services.AddDbContext(options => options.UseSqlite(connectionString), ServiceLifetime.Transient); + services.AddScoped(); + + services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(EntityFramework.RemoteAppExecutionRepository)); + break; + ... + } + + return services; + } +} +``` + +#### MongoDB + +Similar to the [Entity Framework](#entity-framework) section above, for MongoDB you first register your Migration Manager by implementing +the [IDatabaseMigrationManagerForPlugIns](xref:Monai.Deploy.InformaticsGateway.Database.Api.IDatabaseMigrationManagerForPlugIns) interface +and then register your repository for the `RemoteAppExecutions` collection. + +```csharp +public class DatabaseRegistrar : DatabaseRegistrationBase +{ + public override IServiceCollection Configure(IServiceCollection services, DatabaseType databaseType, string? connectionString) + { + Guard.Against.Null(services, nameof(services)); + + switch (databaseType) + { + case DatabaseType.MongoDb: + services.AddScoped(); + + services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(MongoDb.RemoteAppExecutionRepository)); + break; + ... + } + + return services; + } +} +``` + +In the `MigrationManager`, configure the `RemoteAppExecutions` collection. + +```csharp +public class MigrationManager : IDatabaseMigrationManagerForPlugIns +{ + public IHost Migrate(IHost host) + { + RemoteAppExecutionConfiguration.Configure(); + return host; + } +} +``` diff --git a/docs/plug-ins/remote-app.md b/docs/plug-ins/remote-app.md new file mode 100644 index 000000000..83fdc3ae7 --- /dev/null +++ b/docs/plug-ins/remote-app.md @@ -0,0 +1,61 @@ + + +# Remote App Execution Plug-ins + +The **Remote App Execution Plug-ins** allow the users to configure a set of DICOM metadata to be replaced with dummy data before +being exported using the `DicomDeidentifier` plug-in. The original data is stored in the database; when the data returns +via DICOM DIMSE or DICOMWeb, the data can be restored using the `DicomReidentifier` plug-in. + +## Supported Data Types + +- DICOM + +## Configuration + +One or more DICOM tags may be configured in the `appsettings.json` file. For example, the following snippet will replace the +`AccessionNumber`, `StudyDescription`, and `SeriesDescription` tags. + +```json +{ + "InformaticsGateway": { + "plugins": { + "remoteApp": { + "ReplaceTags": "AccessionNumber, StudyDescription, SeriesDescription" + } + } + } +} +``` + +Refer to [NEMA](https://dicom.nema.org/medical/dicom/current/output/chtml/part06/chapter_6.html) for a complete list of DICOM tags +and use the value from the **Keyword** column. + +> [!Note] +> `StudyInstanceUID`, `SeriesInstanceUID` and `SOPInstanceUID` are always replaced and tracked to ensure the same +> studies and series get the same UIDs. + +> [!Important] +> Only top-level DICOM metadata can be replaced at this time. + +## Fully Qualified Assembly Names + +The following plug-ins are available: + +| Name | Fully Qualified Assembly Name | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `DicomDeidentifier` | `Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.DicomDeidentifier, Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution` | +| `DicomReidentifier` | `Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.DicomReidentifier, Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution` | diff --git a/docs/plug-ins/toc.yml b/docs/plug-ins/toc.yml new file mode 100644 index 000000000..72462cfec --- /dev/null +++ b/docs/plug-ins/toc.yml @@ -0,0 +1,18 @@ +# Copyright 2021-2022 MONAI Consortium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Data Plug-ins + href: overview.md +- name: Remote App Execution Plug-ins + href: remote-app.md diff --git a/docs/setup/cli.md b/docs/setup/cli.md index 1c793fa27..69c19cad8 100644 --- a/docs/setup/cli.md +++ b/docs/setup/cli.md @@ -1,5 +1,5 @@ + + +# MONAI Deploy Informatics Gateway Services + +MONAI Deploy Informatics Gateway supports the following standard protocols for communicating with medical devices: + +* **DICOM SCP**: C-ECHO, C-STORE +* **DICOM SCU**: C-STORE +* **HL7 Server**: An HL7 MLLP listener +* **ACR DSI API**: [The American College of Radiology�s Data Science Institute API](https://www.acrdsi.org/-/media/DSI/Files/ACR-DSI-Model-API.pdf) +* **DICOMweb client**: QIDO-RS, WADO-RS, STOW-RS +* **FHIR Server**: POST +* **FHIR client**: GET + +> [!Note] +> The ACR DSI API uses the DICOMweb client and FHIR client. + +## DICOM SCP + +The *DICOM SCP Service* accepts standard DICOM C-ECHO and C-STORE commands, which receive DICOM instances for processing. In addition, +the Informatics Gateway groups received DICOM instances by the study or series based on the configuration. Once DICOM instances are grouped, +they are assembled into a payload for the [MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager) to consume. + +### Workflow Request + +With a DICOM SCP triggered workflow request, the data trigger contains the following: + +- `DataService`: `DataService.DIMSE` +- `Source`: `` +- `Destination`: `` + +## DICOM SCU + +The *DICOM SCU Service* allows users to export application-generated DICOM results to external DICOM devices. It subscribes +to the `md.export.request.monaiscu` events generated by the [MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager), +then exports the data to user-configured DICOM destination(s). + +> [!Note] +> DICOM instances are sent as-is; no codec conversion is done as part of the SCU process. +> See the [DICOM Interface SCU](../compliance/dicom.md#dimse-services-scu) section for more information. + +## DICOMWeb STOW-RS + +The *DICOMWeb STOW-RS Service* allows users to trigger a new workflow request by uploading a DICOM dataset. The entire DICOM dataset is assembled into a +payload for the [MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager) to consume. +It provides options to trigger a workflow with or without specifying a workflow ID/name. See the +[DICOMWeb STOW-RS](../api/rest/dicomweb-stow.md) section for more information. + +### Workflow Request + +With a DICOMWeb STOW-RS triggered workflow request, the data trigger contains the following: + +- `DataService`: `DataService.DicomWeb` +- `Source`: `` +- `Destination`: `default` or `` + +## HL7 MLLP Server + +The *HL7 MLLP Server* accepts Health Level 7 messages via the MLLP (Minimal Lower Layer Protocol). The received messages are +validated and assembled into a payload for the [MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager) +to consume. + +### Workflow Request + +If a workflow request is triggered by an HL7 MLLP Server, the data trigger contains the following: + +- `DataService`: `DataService.HL7` +- `Source`: `` +- `Destination`: `` + +## ACR DSI API + +The ACR DSI API allows users to trigger inference requests via RESTful calls, utilizing DICOMweb and FHIR to +retrieve data specified in the request. Upon data retrieval, the Informatics Gateway uploads the data to the +shared storage and generates an `md.workflow.request` event, which notifies the +[MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager) for processing. +See the [Inference API](../api/rest/inference.md) for more information. + +### Workflow Request + +If a workflow request is triggered by the ACR DSI API, the data trigger contains the following: + +- `DataService`: `DataService.ACR` +- `Source`: `` +- `Destination`: `` + +## DICOMweb Export + +A DICOMweb export agent can export any user-generated DICOM content to configured DICOM destinations. The agent +subscribes to the `md.export.request.monaidicomweb` events generated by the [MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager), +then exports the data to user-configured DICOMweb destination(s). + +## FHIR Server + +The *FHIR Server* accepts FHIR resources as described in the [FHIR API](../api/rest/fhir.md) section. When data arrives at the service, +each resource is packaged into a payload and a workflow request is sent to the [MONAI Deploy Workflow Manager](https://github.com/Project-MONAI/monai-deploy-workflow-manager). + + +### Workflow Request + +With an FHIR Server triggered workflow request, the data trigger contains the following: + +- `DataService`: `DataService.FHIR` +- `Source`: `` +- `Destination`: `` diff --git a/docs/setup/setup.md b/docs/setup/setup.md index 70617aa87..f7e7a794b 100644 --- a/docs/setup/setup.md +++ b/docs/setup/setup.md @@ -1,5 +1,5 @@ )?2pMszdy@inT?x?m74IGDzgPGEk5)SN zyNP#H;0oiZPGY?jBCj4pl~7gZRl>z|Hc-u_+e|=+IVzF$S0Jp74AJ|_L=P__Y_TCa zbh{u+Wl)Qcm*1-Qr+p3>g}Si>Fug`BxwdW`e&wLNHTm8d$EHXRZNBb2Gln?Xqy<6> zjx&Hq2(u%wB@BY^`()y;w~1LaE~4Q%e`n-QD#L}K?C|c|H-HbyjRNPEuN1t@YyeMu zBI{aT!2FyN_X*a=Y=t5Md#+9Gnjid8FGiv)LKA!PE zcXEV@J8==MlZF04Wlkqw2a!Ft? z?*_{nm%Z~MUq93gFO)_%#k^_b=j&WpH4?e|8fKT!7a_17Q2i}PBpMPnQ(8;FT+hYw zSO$sQT6+mGL~80(E!U&^V`YH~6xjBSwi1g((N}3)lu!D3U%}~XGtE1!QkhkaJ=AJ4;P510s6r8hn>txM9q;Mm zkT~E$+U@tkAE!@qFX))Iuh|Pp|2{vXg*)x4FpozA#gd<~QuU`c* z33KD|@7ktqD^hVv>g~9S2I^pqIg{Xa> z5q^cHhn4ULb^3|1b1SiT-i+wezBEUl1CHVIlAT*-Lyq-NhQ>p#@D2`?gzPF!ZW(|( zvzJ`-7&X}%7@7eeGjZrlO&bWZ-iP$z^S+4)SO#vll#So{C+_@Fv=Ps!l%X!U3!lcE zt&<&)MqEu;lVryl&x^&mw_L=`e(_z*DB zrPBM9@JI4879M|KAm>CrktZvvatgcUXOhKQ8Nl;bO{Q_1mkGq$bmQo4-so*irb|@o z0vF!Pa<21~QL_G%wY&K~YDJLaK|i=6X@+-8t>9<{r&zhbyAFcclL4VAl;71;$<;{0 zJZ~T9)><`Vjz$^j_;mY|#YYOuIF)7K9PcEhcmZ^WXV~5~O;! z%{9O9VapDZ^eBq0A+w#bP0orTn4sNTJL*k3yHw=-O2-RIzL&1TGxpiw&c>X*Bo9uR zAvci>HPRJ!WS;RmZZjTrT{9}{Khw?dp;<|QJm>Ho)ni@TqZM`eXsyy+;y$sxjBdV0 z@}ksZlQ%5<&asQWX2ws#ubxVbS!@!oSnY>?zkJf(kV7f3{hWRj|0Mq=(tCywS6JYY z_SNR9y`ku^iFcBU(C7*9$00n#j0GNd8u*q9e)CN*BgtE4hPPBwfGB(7;V+x*^3M3=tYPmEQvIw9n2)U~N=LMEs>}mLS zka?Vp+t_X;gOr${9}l!-hY$Z9md3=xEM2(Mwp;M76~S>^G2%EhGsxhH(B|AU6b7J{ zo7k=>#)=G7ZE_|DTxfkDx#(7hdpu=;kbg}!MuNl)L#F)6$UX)WerL5eyf>fZ3~*RE zQt!;^x-jw|gvO7bR$G8To-M}t$^ryUAHDaXb zYVsktGWydq|D427@m=X&$J#ps&FE-D zh|rLgWSAT&#d6A*3g9ZQrnaUl60wmI&?comb!WSuVYfm{(nGf+n2>_MQc!mK$hF1W zOSN^r;M4FG=Px1#QNO6{a8DumP5?r_$7 zvg1LGEzvU6U|iwZ4aSW+iGc3x8w4&^UFth@x~|S;Jh-ijSno1Wh_w4Nd7YBsmM)rvU{rkSb6M_@1)vcV9IPv zz(1&BfikU=t8y`^w#>c%j|+;G(|AIpN3tS^szH+wgyPg>5Y>Qv&}g(l&m@E)BRvtG zDoy=rvrB8Eg!c;b?A_WO&Le{WtGgB5Qsoe2F183d9{yRnvQHj!$2yvx>^jOpIbJ%j z%X8f8Ii-djG*FacEJ2ICdY@VY__euVJ?)2yq96fWjhUMJm5x;2fZ}N^O!n=scQW!$ zKNnsNI+R{Q3A48>*S4=h4tm}irStIfhUs;^T?xR|AwEx?f6Th}<`HB4tJkU1wZWfU zxN#fEN-K2sWWEeNSHL!SU!*{LD5fT)ZP9emV4POg_H{3a^-Wv4x6;A?#0O8>f=lJlWh5@3sC+##t@}2RG-)$1FM*wPL+YrI~QA_h5?E!hAcsF zptEFf)m}75(cK0UprJ1#{sp zxN@+?G{@$a`STJ0t;uI;Dxm4JNXC3uIl(*J*A*cuRfDehMjw8nP3W)=z2SMdjf^P1 z1Pgmhe=?V?Xy#LQoD^YOCfUu8qFYKLPWee;8CNFUH_$n7jLKb^upO(bnxDCy;u$B; z)2s95?=&L72vsz4CPTvOyM9TU#;8m3Y4$VMAT(IEc_>+|0eNO;5KTN`vl8?2v@kI* zgV2TSx$Y&B;_C=CgnDB!4jips7`z#00q%Ky2$Fh!HEZ$SHb^l#iA1Gg15@?dW zXmoJ#R%0PTyjf&WBSJ5T1E>wT4SU5qt4$c44!1k&(+)XKP6^q% z0h$jEjvG#R#C-B)3PSn*O_it3kmU;2PN-1dQDU@6GCCY($(r%nJ;WK+lM|D%yJy42 z4PRe32?gF6)^AMQ$@tMegDUyZIg2}-QYL9A9pFi`t^5g3((IpY;X>iO+q5NeqPmU7 z;E%i_;rZU4%wc#t3(ZOMvkqyC?VwQ({>r{Q4Q!z zxyN}+Dn%)mk{Fug^7R}w5{$;3Z!ILCJG+4;`s%8E+n*c#+=XY@ACDqDkkST>ZYxKI z>d!N^bL4&inu<(|*iWmKj*ybHjWcEj-Hz;84B;;q-5`T72&@&M* z;2tnX!Br_0Rjk-9OK$Piper3_uBQ`BC_5>2^@bdbMZ4RZE`7N%L6)%lsxX!2M5pUY zOauXv1z+hCZ*iCFe~st+io#zziZ8y#c(Ka8VQVuyE1bfw&LCj@arDt^eHOl+)nwF{ zsESI^%6lwe#8T+PC!gNx`}K7-d!B6dg8a;t@&p8fBDrk zX8Zv7wY2KxoZCICJ341yP1vd8cDw4u{{cKHJIe3W$>qIdyO`Xv(y-`szO^yBd|Rv9 zUI&c={QlLk&dbs9Q>q?WG7UHXVY>UeYma5#jN=!ja({bn6=(&NVvHok?LG$l-I)V-4R{=S0MA&|%;|`OUhA z)Sl;JZ`^sW{cMDc;p^|hBkSf9dtb-o8}@+vlB`TU6IX@tvn`pA{;SvUWy*^-k5@J$ zUN3A){C?Ga&qUU%vh}>1O3OSx?ck6JL-xq=nviU8CW*h(rR#5S9O*r=6XK${H-JX7 z-?S;~WrO$11~#>h z+OQ_LJ{o=T!_h=I0puUq3wBvfz=F_cELS}smlYlTlpb>=T~^=T>|-d|l*fIgbW;#K zaX81&@X|BctYa_i@QY|w@KU8tMg8a_Le)%RGwl0qNsJEMpr0P}5thB>aes;{Dj?(* zb6yrHGs;wPe^@x`2z}(*=DhdF%>aB~EEqUksB9Cxic?DEIa{&G+bhvhefQ*nB>;~`1{U-UE(@hi>psA0IYhDlN%cN4)#={kF3g-O;J zeadO`qP38r_xN6|`A5HP&P1Py2v!LHp4#iI)EUD?rIkRHk)x!2F7*=Q6#_kJp32W# z2Ts2JO}}Pr2aNs)Oo8PEDYCu5@08K3IFf-O)Ii2Sz_3B(>MZ@ES=Dt#`(2u~byLTM z7`M*V+I3gOsoA)Lv!2TDyHe(j{;y>9iBGZ5DodfzOWzn-%sJYi^%Vjj&v!9;3yt-kd{Sav z`8{o&OgRwmI_Zal5mE4Z^Z1?hUl;apXqPg`fE3-KHy7))OmOBgZk9{%u`^XzK1R??IYko{qn~vPuhs{2l_hgc50GY*&qtuMf4DAU*v;#6>xMzL_J6KH*$Yp8 zP5PX4_nIjNSF1b_;WlFhu?xe@Hav5NMEf@t=r+HsK1l^FW%Gv~kz$1u{4}UC?$mn|Y{UGOdke(29Yg$DXmVa< zAr`lGQq!}MUVV%1Z(w`zg}oGgDd%-`nQXPd$_?o=X-_wcKdjm7@LG|KCR2OdBEHxm zUAN{wHM5I1=n{J5Q=Y-iZYA^G%jjE{;B#1%LPu2-IIiksEZA7ZzH3fCTM6(jHNFkqM=l?x$af^GEMduWsol0raGv8Nqk8T?b@tJtkho z>%kYO>~@j=)gG7eqL(vP!hpyxl$wpXPqjrfpI&u_uP6}6-SdxV#WPpG<&0K|sY}B8e2s9E- znpBQ`8@9{oU`d_oRJ9F_gDY3v>BC5V98x|zn&7?94nG;x;aw=oPjin*Y z3(;53KzcY>^PMiY$;lAiiFS@*dt8xJ}BM`a48!6jDaga195%5+RMh=EPDmR zC*Zki;L~>d@nFD|QwOGT#Ld(w|MYQ>@LSUl@YAo&lhMA$D0hqwmh>4%^}j}T>{k%Q zE0xaLtb?L%#hH%U=TB@biWr%bsk*A)X7ST;My$N9B!H zP@owj*^=DW%w{7=O=WWXdOS2F_Cv;3oD^(9EsHVOtu1!y7|K&AB&hO+d0Fqr^!j-5 zZh%2@jP3{7D>D3n_~KBmzV6PYlCe)OARH$}xKX8qZ$8c3zgW+iD#)4_qYiyRNJPxO z#Y@EUc`%4Mo-Wo6b(*!{alw*xI31gpcK$YE*ZZ~I6H(r~+s+G7u8*2wyB{?=#VRrY z@-fETR?!o}ac%;o;$m3!=>VnHgVT=b{avTvx9Efvoh-Ziw8X!s1jeKhyRWFp!RLV@U-B*;soNAR6mp>tFm$`s4o8GEU+8%NAkVdc5h)x3zuk*yyJZAEWp4x;I}Mn*aVb z9w1l$)@$Lg`*Ej_gWXi?_#|n2hq>A^w)SxzTih6c@tgaxdO#%XAztjN{I<3Fcd^+K znY~|0`g)#5{IT+batX}vd~+u)*VFKne>iv`m>eNn{=is@ffOkCT!R&1o4wC{6t~Cy zGZPfGWM0t8S2M-IG7c(jm*ofGtQ99t_5{cQhRW=&_6h_!g($H65)(6*y zYA$*zA}wp&yv`>dJC{!uGvT#ch*EE8BR;$%Lq3UiX6jLkAPKEJ{UR>f8kx7qbx=Em z-P3oM_xnKJ3fr)50*Sn{_4l+A2cgGa#*Y5{NUwdbEQLwc-3%%N4@XU~xxBfUG9Gpc zQbHw#cc7u4@X8ejSmEw*Ks1gB|9mAR?;-d%{V&5#ft-fxwL__N;L*3pCts3F9Gx$= zB1eLF40Sg{f&!AY#JnEjW1*<*s!pQYr(clopEhQ+4yiVLRONs6ESd@5?pzWN#QlEf z&)b?1WO*OzB^nldTK9eXW1eOqHvfQ)oJor{W*5ERQ%c@Md~)=G#YYNzQR@gX9>0m8 zp2AI=Xl%6sBs5Vc@26f%e+shk(ywhK0|Q(S!HX_GIC9*lzV^94Jd#~5zgT zB_RU?NOvP44MR!@LwAQrry$*R9(>>LJKtGnEr0lfrO&?izOH@kxNkuCCoab~`NnT2FJaf2zHOGj#XyZn;;rz?C4Q|BycS_UzzdBC|?>Lny}YPV72*k2GGUp#$%Fx%+m7yR+`= z-}7zNZ&KpddfrLzZ^30y7Xs0#q^R<1lnDxcnDSOd}xAKcWk(Bta_}G&~1ZjAG z4Vm0tbQ5);=e+BcCBDwg^+P$zs`Fm?_KgbRVs+&C?^QRo$W(eH@1efm zA58IRTP{i_1`>1gT$Zlh5QWvSbzCH8%<3q;jnQgcqf72i&l^ zI5Q401T-}Iku8_A$SPofcNl@UT(tvRNcM#^xum-*lg#wguw}z}4Uk(hO(vz^lUdf^ zvP#)W*F23c6lZ1UCpp&BWf_JHv6&9GVvG*FWD&fU7Pl?TG$jChewdmHnyXdi_Y*V+ zA%As=a0^)7v&p)@m`_^GNe4fIr9nNGw0>Rms)_lb=F5YhN1RrV%pACTE1kIs%MVpKe(;&>(hFVwG?;>_ zTg{}i2zk}7u5%98`Cs#Fab`$V)V9gLy0iK%esT4f=Mw3sdfl;hUAmb*?e!_v?_KLC;MN=S8Wyrl&t| zb#x53{^3w(=vaKdNg97+;0q7>^65_*xzUGibhCT4-7AP;k5I!^KF7#*x16)K{b=_*mKsBhy=KXU0ju-i3s`)UxcC!)GquP z_GE5QooXw-e!k`KymNbNWt+?@;?Vn(uYQ_duOvBh>(16%P1hI?b7`f_?Dg+3!V{Z9 zQZ^3ow|fXC4Yjwm8-kYYKHA z#W@DGcYu1;=>b2|9sIGJa7pD{S$zg$ZjZ>u;pXklGUz$w92fkFhRLWk&&qp9aro1F zTJ=*^bzz`qG3eu8$)}Er6F5E&CZ{akiWq_TmR+qQQGM~}i>jq2JydROVTqb9nVSXC+?GLWVPn?2)-0T;_FX#F z7g?js!I}CsD`CLfm(WQ?%9hUU{&uuMi8Q_S3psjx)`48k+}(8-=JHg7i=`Rs(9K*a z-%jcN7KM@F6rnl6KPz^AsLguj8A3e+IrA~==vgNRQ z*!!oV8cRPhJ`J3|%kcZ;Jz*n0f8!b88MhSJpUjw%RQ6#M<}w=Pf4;OpYqRLhI(z!F zi-~ZC!pO4A##gigN+Bm^Cn4OUky~OLp}pVY57yZBQRE3+o~m0@_5IpD1OWW{e2)j z3Yn`9DHu=R9YCF8lT2uMvmXAgnEEl24s@ONrh$zR%saF3g63F265&`gHOZLjqWe$1 z+~_B%z_C(zg^V{YE?#t+_pzbgKMN+k3-b$UV^UflH|g4*_qJW5y)n_cmC`@W4g6y; zR7au-nc_EC<)5m%!uMWs`VS~B(!#nw{dL`zdTY!~{_=Tn))rBc>S!)S7nv@AJ}=+q){&@U(KM_lO#IiV!U5fS42R5mgMR~lA& zuf#1|Tirs;QQh$_qnAhxqT$NDNxVW)2x;@=P@;wFX^u8t7>8HSLxS?r~)W zbHz7buJ@VkKWOkB4Cj5)yYkMNr&f7YL4!(jWU5K4hasNX(M8v8!=L;8s5IK| zzN`;MiHH9*LuWs}kpCQ%VbS~Jnb7`%4#V~z&OTYA&xP+gx?AUVWG&%W;Gk{muNJSu zN)IV+b8ww)##EnU2Raeo)CCQnwac$hzx%;Yg#b2_bNkKd7lKe759 z&F%XBJx5@J)=ci$bSNM9cl@6@XS_!AIm9a6=t_qcdTGybSxkiG14HzK?WDhh?kQT| zmdNVO6(=w}EsT2NLz?w2|0H_saoW;Agq>9!8zY3bqeZ$fB>d_Lh4zf#WUGuO`2)p7 z6p|w#z1_``(d;5Kqv)R&_Q0o*UqhpBexL6?vxHfU@`ycosG$zEi3HOs@{kvLpy~}&%3Mz<|k2#8BL4?lC`n z`?U&498NskMonk6ze+8ClhN)uML$3|t)sj#$Il)D6*EC2Kv8ZP;( z!(FMsxnMlYg>_mgO4dGdQIb8y_)s^O>NXjI*sqU5kDgG@ZxV@-C7IZ+#8brb^S{V; zd31Jj#@-c;5uc!fn&o1oZ+|)2SHVMAgV0Q9|2VBf>Aw|akRH@Ryzgc7UNRe}z@7b` z*Oei=#fiqA_enSXfk)B3r=#>cP)ldR6adw^mOXdsls`A~WACbv!12P33dYX7d>&Nuw0LS|j$8>=37Fv3?IR)BiT4m?uuhq{4Z`@Ge$Zj*ys%a-kLEri;_AA#v~|jK zzKh|R=q+vkAcruNz@>}`jkm8C+d`mg!6Q$4gn?$&8MBImee-?3u*N*!8&%`wAdA#h zL-!xb{!*7K2XCKT>2(;gt=MZ_Czla0&HAq>c?>#420ZvdSmLFLJ+ft|2c`H;EB?6A zj??L=!rn(KqkPoQ=HhR3MmEiL_6SzC$n-EL%0I?fmBGlqH#907mCb5MNnHja$^TJ? zQ7YlcR8k$m)j4i{t|IIJ;vcVrw;zhE=3u8FaTk6qWiemW%}Ao@VM>ilI<+shLFVoE z!#HWVn;I^1+0}ZnZ9;U2dhv5yB{wwA`@k$LF|dqAws>h?<^%%1FutexaS)u|GW^RT z>SUWxg-NW>?N?~>m56N|lo6t7)(AhPEx4k9{&ef_H%@4A8IwP0lWAoNxsDn(y+A(3 ze{mn<{ZZ@bz>BNt>njyZLo}po^xa+E0e;SHrYw~-4%Uaz9$CLD%`q8inym082*Lfr zr5~(tIgpPdVddg3)|_F%uXXSo{L!)H@zZu!!3e zH4013f7QU$W|1m5NPJ6CpS7r?-d97O|69k^ zFN;W?*uCDJs4@KUXLAEQx7r%EGZd&T2ugg+s%XSy?>)1LHL3lT*O%maWC9voU7r9@ z7TSD5mhf2RdfVy7`)Bi7yt5cV=U>!IB49BNG6D->5wdIryR;KD_~GcJ9G^CO=(kai zyJ{V_=_MI&XF3GaK5{ImL@qtPKUj6WMI4~wa&{fAEcy{K|DFxR$C;?hSwb2vobSe} z4((^(9}NmdovIgLJ9qolY`%z1Q%~NuSm<{9(PgvWz-ZKbJ?(!TMS6&J2f8Au)-PWm z{lN+}DWy;TK1=Idspfa~*;X7^HNG*w60#D-ytV8xm_Hf9&Uc^g;W}76lsCCl_{2bT z*K~YeaFWdQf!Ygi?edUu!SC0GLCd{wqwAyVyV+6M&AsJ@kHB1W+wcA$le7o%#N-`6AlI=l2t8OdsWo5q%%!cOM5VnC?PJP0~p1yLzQRoqk;GAJ%F=rf#wJ z<>Hw0{wwn4w@!6!n+WM)11F`xyMkc+TdKB9Om=LE8{);QDP7py{Eh=I0@G3$mH6h$ za=;Dhj0IB+!OG`oYul?V0dvQ|BZ=2_^Mly%)c~D=yhG3m59Dk0nIE-zg%D zlV^Z*%;&z z6V{_N?iO%3;|2uo@bGU0dAE3X=@gp1$#BxsLMW5Iu#N z?C$d3HTiL*ksgdgyQIY_ywgOO+$0pl*KzOtMpQT3?W^*8+?@Y>U&r^%*GPC2?C!|I)Q75f84hZH>Zl59_6ACHrOCUaHV zSW>RoTg+E-i}8iBPR78~T`|@If`Yd<8lG+zKMr=Fzc#nbthnV;cF~IS=Y4dBaXNTQ z#gaUbLDOv>7$(d>9AjYXsAi@glg#2&%fapfW??VIGcJp=-iEwB=+=@*h3SRq`3rSi zrxeJXp&!rY%)L>^MdXKsxC9Gx?qmAdG$Ph*zgzTo2@9wG>KiHyv80R$c!%?|-s|zo zDz8Rs!?(;kE(DF(o$gWhvROS>770fn;`MAZ)MBpUlP@qkt+m^`J@WV~ zCD7>?IGV!!GhUVcw%{eJPC>9@Vjem$*l9T4{08A#{`Ca==e_(1;pSXv2f3=B@Q-wi zeNYGb(zbC8ZlI}k68!l4X6feP%KFB1v-Oz6fh_n(%i_o0mA`9&OTD-{KcLV#XvG4z z)5h!Oy7H1MmuU4PHcWCJmG=gS_rp6%tBx;P{Q& z$;{{Pt6BXL@avhf(+}!ksjvQv99v4gOK9FsY=l6z#n|Wx@pk>CQs+|tQ5DSMcV$)!4e#BLFE3~~w_M)}}*(j0=G`HnG_ zZDbiiUOsec!NK*)f?Ndq=-Zcx@zl$(_9{AJ~H{wvg?_ zGUzN|X8TsWku*w}8Eg6eWXbkk1*A4;p0D9;X#YGaDxdOqf$d)A;=S-pG)wu%2w<6m zC5m%RcihE4_F6dzR@!`ByB!;GAG5|Fivn3;;)Mceb}>CEH_n%y<)U>${ptYfq| z$yk!*zu#+IFuJ5iMtyw@&A9=T&|-yTWf}(IV=bi6G*JWo7~gtBr)rRO7>iT=yhDS> z7dlz}%GNw@k_n=A$yPH#FA~~l(V!L&IKhjAG7a|f8tr2~1B(^{KSy3&u@0Gn0zYGS zL-94S&p%>T>`~VcG%1NOX7Kch#Uh16z%h1hQGu8Nqlq zsInDMgX}i}h27GBeDJkygGF+Oc9zM`cjVyq*>7%EsA`MV8e4DLFJt zxrK~`l0DHU@P`Ir?K8#1A1uM(xp!=I%v%m-N{8HRzeg*5?9?|W@=Ft}gj@7ir}9hn z?M8b`KKD_rkmmu@Ji-AdJyfC7;W-zI|Jq>xV1s9^;`@S`oTWMp6q|5%h$S!KdXJ1g zq&YKawYfF29_!x>?+I}54}bI&GUQ&bej~Ei{H4J&8!{WdPEjf}R++CsHBTptHo)X& zN06-e=oYe-v^Yp_60r+I3dC9fz%?zOXv2^-2{q?1xY5pA`(!M|7v+9@Lq_BZhv()8 zM4KmNNuzQmj$l#VN7F0Av))J z>jhK#TRQtvH|CY!P==XRm;UQ7MA1wWSx?&af<*Jbo-jp7#)71rGJX|0SyD1^ zJQyO!n7EQK+8Ylr5Z}8#NJ63&T7+Ye6{4T8!+iY%R=ymyIfDVm8&i&T9IIbl!xUt5 zj1DMs`ps{*hDPsn?X@^p3od+fG=$2qO4Uk~7)uWgUXEj3S#$zLc$vmm3$Z8RoywVC~ zvhvh}N>(N^xBYr9?4udzzfcBDv1U$}N3S*gG9Jc2prmaiie9VgdAn9}>Vr4(K24MN{VMv{M#v z%L$C2N(2skf|%vbT+Te!33}x+Qejj9ez7SJM+&LlI>MGLY~#gdmSzMXT|o4&qkQ|- zVqM&oa7M3;%QVEcwX=H(wH5azu%8Xa*7tG;G|BcqO@grjQTG!w9FSBPW^&t1nymsI zvBnq?4{F$or<+1cT**NDsZO=dCMlqKjixMKq;=F8jBSz}!HrcL?$3+tN=q058FD>h z&{InWhVcIj;0R?V+k|W#nZ0db2#=^J5z?56d!rMhy;(5~kyg^@Q`+zCP&i_n>Oq1T zb;TZneMXEYAy;Qo6+BtR5!dL|SbkY=2ep1lTH{i|RT@Uzx@X5(g#lR}~>hIuk%2}$T z^ABZyKO1ZVj7%mK89_0Q%A{xiPWmZYHBqYEL1zfN)ZVtSoVq?VucCT9_HkDh_~>P~ z`K?#kBO&t^CY2>n^?;3SlWwOhTPXXfJfYNe^oClbJK{pI+3_{dyTjfQ+N{x<#Y+lO zP|18^xjW?n`|6ovpl`3t%YheFo^KUzx$shm*I3KQN9J7FY`)3`1kxnW zl;D<+X6Qgna0qe9+HM?p%IujzEm?3_z&l;tz(n_d5UKMxmM2;HLOysdh*Y6sKTGJ- zah~e0h%QLSL4$ys2qx~h?4aG6*-I~yRngO4kHDyG?JLuuRXk;Qb!Wn*zfdqkVVAJM z4SE8;Ikcc2U99U+zY_XKG5~|Z`-K7_R?Qhr3q&_wzi7KNEo=2keSoDm1t+C9;p#hg z<9#@?bjXZrxArP&((+3?OUGBVngldEEYiKTuI1~rx@#YM`7mi$aw5YwM)qysr;5Bxv@DK z6YST8CagZLZSiBQc3qv^Kdi|0TqXGDNpi2hzyDQb7?u{}(q6IJb^$7`4E%}R@wLQA zO*xZ(@iPKz-U`eNocQ+-iV9}^L$xd5qnJ@(vihHlM4Obqg;vPldr+OL;xp1I&5}7v z`j%?m(Z*(EO)f$P?^4amfDM?zOD`4b(Gk{Dd6I=2-tkgX_B6P^RM#N~{;PR8X|HwnnL0@L7v***exuZBa{CduI-Nk4< zFviK>X=o{>#1Y3)aSwG6e3O6z&|jp)c=Q-GNq6|U_94Q#p7{Lw6|lwezcHOOwf0k0 zLHID^2@V3>{jQ%2$5EbwjM1Ax+B71LzF?zJ6+x&a+u!SGFoK+MdH!{-0qdh4A^mmW z)%QxIiD|sVJL_ViHlp2qSSWAbSWdi6e(8qBJ_=E@W>)#@N%m4`{=r+;pZUm&=An&} zfiYfeWiUmM5&?GqLwWm7AGDWB=%Yj2cP)X4tHNj_OJS6-3!}qKRd@N!gDHxI2d^&h zChrb6Y0_BhZ7Y4~|Nj#&56M#_B^Fzde7h4Qm0SutHYCo{S6k)=l$1Ktq zo9NNI2*tYd*ZP-(P}oXZvFBSmUZ!q*lc=p%`MF;#^!v@LIAzXK6J}Jn#_gZPNHhcv zFX^Mu&!f!i{5jJ#(=`vae^ND6Z)+h=Hn<%X#3(V@FQn7Rs1)<|RJ!^9bM|SX$?s#M zrz|b9rg#G+KHTxJ!A~4%z-Qdf1T{VV(9fiIL(=&+?lv z`BG*gIFZT88oW{Vg)V*)3kpLM(0Ny8E>qX`H#}X^*O3}sFVBOE+05%lFZ)@IyjPdj zDz&4{6Vz?ZZB71Ux5W5zSDENx7m<8rBYeZ)xY%$?UYu*jU8|f;!JTpz;M;h8sxx)Ot&_DcvHplY#w?tLRm)BsK!DqIsZ)gbet# z?qs@Vcyn)5;ru9@04C5s9*-0r&sebn#k~nvX6dd$Tk2;364EB7V=eFf)u!b?PeT|; zKqC#SpI403_E|e@Pm^mvxs$SrIN=gj(x7cC5>i!7Ovb2no&g?g6Y%&9PiF7Kf%N+@ zv=Z6PobY#Cv;M4F`Hm?BFF)ne7Yko{0SN!g+T_TCax&%_1x8cL^_(XdzXNOf`zHTRIC$t5N&gKCy zMuPximO{`OW81I$Dz+V7yG(TJ$SwnPwK+BUY((hm$74bDu6$~ua}j(Z=oj}ZQ^k;| zh;Dih8|+()3|_{;0D!Z&QrnMdGw^SbJ8lG7{V`&n;1PPO=a`Z#It6zznR7~mzEW&V<1A>Z;h zxo2{Jy31z=m;vI1ca}6J^?R{WG`nQ_a~`wn-csjyher{Pj~^m2C5Sz01Wpg>Timgd z32!3Pm^rC-=mv6L?s1r*(XpTPMdqA}-O#Uw91H+ZDhHxrn^Iz&DvNfI2ZW&wD5cwY zqFKV#qhFGq*jY;D6TW!E9vMG6x@uby3GLtNis>SwJK4xub(vXnS{LQwQuTG5wPw9m zyA*xV-T4n^fYN8D7=M%F@?=uirbM@mc3}y`%I}`$nljv|njtPK86wtTNpd zou{5daWG5BP7OL5;bN#N`47)>SQVCp&O;><$n`8RjQau+d_Us9pbZtFD(Pe#+kCLShlq6 z?G+4;rOlz##0_@cXc7+NG!hP@l!WiPcvk4dC!`#DX&X{Lcx$5zFT7W1ToR&OodE3? zU+SK5+I-rKQ?ziCG9B^Tj#8?8A78H}!pHadCHnHAmZtt^$3ixHXk@%P>F|mrluzm1 zTG&YHYMQ!Bv~>oV_ReP@^WDx1=-*b@t<$Y;F9cj&opU?iX_1|$w16K28zwe?GS~XG6>j7ki6VA*bQysy#jhIWQG#pog-%Vs8se(=joSO$| zgPVFXQ#Xb(GfCJ2VLrgmH~V<$qmiD&64`@bH!tn(fOn&*JGnzScGvst(~xmO(-;z5 zl*!x2-)&?KltxKoOhD9%{Q1&7A${z`aZq_CKBsCSJuoi9m%UV|7`IQthp*qXp2!A& zNXn;d*j2gUWVXcbbVn@A&Rk-=PfBLGU*hV@OQv^LhitvCpy&Cw-ATc&*RL>VZP%w# zj6(GCV(CjMeqGGbw)RLhHu!ppVN#v?!Y5y=;?2?}l_4fH!>;?MdGM(HY$ghHguR{S zjg=l91p4-G47vqG`4KEsIKMKSC?7=ky;Ed1T#}+TS#q$UWCVIfO%i3%71^+xhW9bz z@5F8=-P%{A-)FbJAU#pZV2d_|ImwuSx)Aa!`4@!Bf7sL{Y`@Li6y-Ca)b38~h}CTCKn$4_w2 zBL8C=O~k;@g=DyUs8dsGbG zj7YWMM+s4+_raLtXy5b}**srkDfoguxFnjJ+YniM6feQ8${W?GE}QgZ>1{G+05*k2 ztb~{J!0=pvPOC9H<_y`?w1Aa8L1F?*w+%73zYl?L#$<$_mzb~)9vMe|DlgMtRC~k< z2YWZ>+4{Rv9!n|NNyH_RXiksG?NO^8;q4^RJKpa3k;&i9P{Oq(6YT}3%{hPo9iw`J zLB`*Qll$?5(?$HK7tG;)R7iuN$*%LOaTCSLs6T?(4;kBZsw#d6FckrqiV6?#frNtsN?dUiREZBdS#MxibuB7;)-@vE&&lN zcDk7ohv1U3WPRV16x=2I+|{TQ&X8gI4ZPkz8?@`Fk$&5bR}?56`EITlNTb(-I=}MQ zFWOpeN{vGtx5D-Wnvw#ykiv^9L!_5J*-=*Xuh%9&T0Z6R)FY~ z34AV9A~i+!IK{^H4O6*)PVxVnDsE3DDE zLxqlUT1w97SwOa_P>@_Qt=g7Vd1anwAU)Ag+Tz}7=IbcfbScdtB$gZYXbm=#xQW+m z33)aiSGmH1n*Kn>&nbmH_?)apBnc0^KcpHfN$kLUK64yw=TLK8sJn941kqx7kgb_Z zbFOU4!GJh~`Ox&Ap>;M004+$ExSwQl(eW-`ZBmuFSH{x|QYisykt2QX8-d!M*y0g1 zk+n!6vIGMp(hf;*pX<2cufC=zVy{vr?YQ}zCxfuR{=9uT-~#&E*G#EA>wuMpZN#Dv@r; zAed|7Z|BRAh!QNfm4_fq&@cqF?!i@}XjgYx%l)IOx>+RA3zO!@ciEwZ)1+r&f)CAi zMw1#vSW9d4w82n}Jm*3De^SJ({^smM)Kw7o_nvbCY4I126hh|9Lt@N9rdxVYk$xkV zy@~od$OBi2F`0h0yFts7fOEF4z7Pv+FWne%jU)#A0q5mE*JI2b*!dF#3a_;VEq2t? z)6*N~zV54k%M*0e#6ADoq!qBrPnn@~?4*<;>0O71E9}$KMTE8SkVK!TJ$q8F$`L1b zcPU1jS`%U7YhFO^f;RygISuT~BFj0`p6@Wn9&31yDbBGGM4J>jf^2R*LM?C!*#GiC z{Otp2HlAxpBKz#4eggLaS$3RVmXNh(Hi4_am_1<*6;}8K&CAq|m9YLt9h>yKP5RG# zGG@PS28!X6$H7iEAAdF6q(kM(23 z6=>oYzi#^UVQFj$?mm9s>GSda;HRH`2pSIgS2>^a3$r-`6g!t*kCt)>{P#e-ZO(as zib%C;7Mw{mqwBNq18KK-jBuD0(j0(5Qz%#!ZOUaY*n(y06a!0Sj3HZQr;6kdPQ}O; zQ_CheY&@JEp`1@8n*uQYOJkORr_Uw-mYkhdwMA9ZDxzp1G-*-b`<&+&&vk?-!HXoZc>Kz z)_Irhbus8EReq$ZCnjq@Yi`^C^a~On!rAvh0p4>5`Kv^yK`vs zC~1V_G!%NZf>1^}QGxDrtqK3I$SQR@_7Xgzj7%ORagqugdNHus?CqCkux4Lnr?f|} z*H2mTDNp3e?Xz2ePxc3)W4VuA=kl6l6sIiJY{}&hK`wUg+3A zj6{>`dH>CCfNmK=)+0|u6VKDLVCj1vf5TF*0)Nq%4v41I!b)pJEm_CuZJPXwEg)A) zN`!Jozr&V~2NFa{cl%@aBkE#)dUpRFpAQAW;A3x#8d0sROgqZf$@~!F8DuHTu1}A$d46~+s+QFpJ_LzHw-JbWX`1`-@Qd7$8ch9AF%4CetU>^B0CWT5R2-cw&P`-(-1SfbbF0q5s zRQ8k*$@q^!k{{Mjgkw!pwFmxmkqtd%8vTLsdX-)IcvLgeEj#~to&?f}|8X}{ce{pW zcP(Flzq1X4;v$}HAqW2gc(Wbj2=V$*P9{5%Vq$ETPv%y?pOUbZkd#RV)yCc2@vHi# z9oxKl#qZy5zboy-bKzYN0R6aI4|^_V%pYjUXa3C2cFg|UlmFWmJNW#Qfh^LBqP z$1&VqrjwtEaCf|wTE{;j6QnMN6*Yfnet#217!F!~)CDedrkO$c*49tD2iY7*rTB8V zr`O0PwL#A*S<0D%%ya)ctbFLR8;s6=&PwQg#VyGoX!*pn!ioFyE?BC#Wrc5*;jF8g zy)-J;i{-5 z=}9%VYT=eA8-B;?up2AfbuII%5m>OW3_W|7GMe|Bx zyf4M<=&%B3hm^#hat~Dl6{bhQwP+LlE8g`{*0+3&WWtY1Ea)_~yGOX#OB@@LqU;Cw z{b8w0Ddh&xP5{D*bO<1|MACoV4qRw<#v2=yY6Fs1!Mn*3ALLm(kGE_79!h|-Bazj7 zuJ5{L@!F{&-DyuLLHH*0gZ->e#YI!rwt;r1NQ0iO==_@lhz_q<-l!S0gq&)z#y*1X zFQCk7MWCcoN4y#?R)^r_cc+H)_cw8@f15HNM)mYBhk&l%=1R-a>I$Hms3M1uKI^LI z$WLiD0PQ)h>%8;>Jm z(Dl^p7x=Sp;ibqpqRC5cyw2WMOygWa-%CytvvLBsJ-%8?#^~Ajpe52BSo9+ETAbp9 zKin*2PYt{~x4Y6IK7_N!Psi*AwIj-tV_aRpcAQDHjzb55;e?ZMJu{Uw@9<&FNTkS2 zDeSVo4L{ISHsAfZYRdRG?@F4@SwQ&k8vKXhFGwW>v_8#BvcGZ+G8Yr(V^G1nuK$vA zS&CR~2XSaALK2dYmXa_~*?Yq#9fO|x&j`MqBeM>D0%qNlgAQ&P2OXU~>4vUa{ zzA|{8m42dvuSLqx6q7GZ?8a#6*0H~BFsY3>=s0Apcj4J_+s7j#&jKIma+q4U_Z zaN)~2^34QUj=^r^p(nYtfBPq_^&4Fdvx60GO|2gl$ zHBCk`UUxveO+4h*uP-s_x~nWl)y-`yjF6q`rC~tWEarNMPOCLkR9k@v8X+1@uX#gT zd7VDKr~c|YYx?fja#Z!?%b@G$Rx+$6c+C@Cxn&^U2BI`|@s$eWT>fcORUf z=s#WZ{`_fn`VVh0?tZM3wjjer^Y66XitMsP-xq&SW+8B`c_?1$UVYbhfPoC?C&G2R z3#sWtM#65`?>vBz;4+?g`~ez;Kxi?c8)vla07WwuTFd)T7{t~i5cZMnLrgjQx~+ln zWrWhdSQ>PUJZ7t+XDVtf)vc0Qs5JlEixFJ!+Vr=l}^i{c!?PzA>$aXaYiw2K>upX4-zSJ46 zwc0&$^5V|-3TX9modd1!B^t@=1I}_>y_KcT*ceJr^$6sd#JLq32I?rci3nL;r>)pzqxI(%FcDJ z2G~QYF?($HAT0<)F;^O%4uARJ{9?@zGL%ja%>eqcw!B7w9tEB%t+9S8eEzPhnB+NS z@$VZtYZw*4cI}-w%)0!mJr&Z=3ZI+3$D@S|lKBfnK7Sk|uVY8QW-$4MzE)t_a0 z^^ApZ3ZWZ_!}^rfP$qxUQjR9@gaGBJ@Us}+8EIm{yCugHQb0DehN$B5yin|u!FtFm5ZojW6 zT#~-qP_{cRP_|o@g2vFGweO-22Yle@bfs%W4;_w;efGcvfnT@9hX2}*yg_9V&sS4q zq_se1Junt;v3!J3K|>2ijPf)oh?HaXnJtpJ8(kklw!agxVX9H;d1NbnJ~+uYSU!&9nMsbcjEywUalw@L||7eyhp1 z!i{S$cXk=(DbircOJdPe(B9~UEe)wGsZL%k1*3uxlu!adty({5674)enOfNMM(Krw zZ}9lr`lC4^$4Mj++Wqm*R_rCG&5OjqNc_@7IUodgAOF|L49ah-sNxTK2CWMKmM4>& z!1KaMZ9_IiZ4gU&VJWu*hBD&m^z_TB?W@4$pH_ybr9zV&Wf1EQG8SNLrL?rK5s+q+ zmz(Q6=1`{>EBF_!5uI4ZBEpS%;aC6T7ZU39_GW~;{aS(m$}+N zFdXwagIBhGE9%%0Pzd7V(2Rx3$}fuCnY7;sTxA#&ip*EuuQDjS_Hj{E>m>kIaM;7FiC0hB}TXo_T9EOp{zjQRB!U$$Uh z#(Ob$_AXjVd$lo?HrIt}pKZZB>+GN)kGg&$V2!6%`SzuaZH6V(>EWJ0znE;jzyxr; z3t0O<1C<{;FeyqEH#gQy7LgPXFOO+BNJM=jQGIK}>A+D~Q2+VTQQax0Fyzv4b~vTT z>FBwgZiaJ>U}%RHziXT6$H02hoqFJXg*o#KwuYp$9K_CexSfLYbH-gX? z`&(Z+{jN#=-9ho7s$W0E`Xa5;Fp-cZ*oO`^gzbwETtwXUD@^Z zj=y|Pb!#9?Ed%ng>l=%*Y!5Z?XuXdOptl`=ncXo|aYQR~0>70kD^@meotT>$jjT%E zBkR2rQH#)AchwJVjjdqK#&xU?EmmF$2a%Q2YOE>NRpG@p#Dl%KhKpGTM|8P*Xg)2#i~JrgD0O%cC{SZChNB|F8XsB1TOsM=8T zq92*QTDPtaY(BVwp-*g{ZB$FwDpm+t2&@H;!y%MkN1TyW9F)6TV=lVJuxKZ)nKO5h z41Rw=RI`Z)69Xg4>oe`(o|667Kj+`Zj7lLvmNlI50zBrbQ{*yta?<&?i3Z%I|$ zT&_=X@!pU8WU`M_k?_gYEm6^HepvEmoobk-tz=Q9Y%PvZELSG`f~qFy_+^x1ft}P? z8+f+zH&}&&qHYe^J|Z`TQe`w?57S${vd*Q_G_Yt<=hNx^Y=4s?Y$Uru(8Pc`?%W%ohGo~;n2s70;2Hl`>AX6?7m*s=Vx%>uq_}=M`lP+XzA^ zocXg6-3C!GQ+$@6y{-RdRi{aajG~v~H3A0IR2mLl;kJe{P|CT4eWY($Oc#w3OwqIg_X6FXvIqwuE&4>GCGJz z8l55acS_}JV5lP!wD~SH`*Z-|KmfNOPwQAsgvgl%b+k-UoC{D`IeQwua4;`a)$cHm z>ZQ=C*%!lF6C%DV*3QevW6jX<(KS<;Y*AuIkuY(fdJC+fVH-v4{+ntNN?QT1;#@M+ zCk+RSa_uA`EENNH!scea;zDLU*nxQOn*rkTE5E-Z>G8n_h2(7R@{Ob#>ai>qivImmhh?qHCH0Le`{5auq7uyAaY*JWfV72;rfz1Skw87+in zRPUg<`3M3oC?4t+BWuS0PQpz)|I9R#Dh69_9EFH~u5x{!dM> zMPw1I0KQ(I(hWUv)Lfu>TZI`iXNA$BU;A#wc3?alSSR=#o}|%TNL(2qQPP$4e^y8A z8+pHYSH(OE;7;J;z^MHof~Ta;9Cgl1Nvm3 zxiwgOWVxc8>myzR5-BmQ7MdK6L<#CdJldW5AHm~S-R~zF1`iGGa0{zAf3I@V5VrMN zz&>X=iG2*lkXgX21kW>QtFd(@v0F5m3P$mStf54YOgyRrRuW-Cj1|i6#EFe1%qi_^ ztZ*4V8BkbJX(cGhJwiV*Fb=}+ea0yI)7N&MJap?VqbFdY8GiWP##BS*c(2U2c0z5rjdl^QU&fsb%Ez)@PGe zy`tq7+bDE!ZK5Z*sICm`9a*An(F7#1~fuv}HCVe6t3mY%1ez0`?m(a!|&z1NpUmC0!zqK_~ zVR2{1+NjubV36^=cynrP=rm0SJ_Gc_=VePfgX2n@HBEWhuht4*sstbr(RVEc17!~eT4 zND|9PWE8|+s8xwYizg_ccLYgK!tpR4qmx!bwaS>#29193G~mDwG-h4VC<-A$ysUL? zBjfM3Sp#aVOFC;4wqx>tE1^HMC9j_&a~pK*gNr5`iE5r*h368VA-Vn^!rnTns;>Lv zwFm)0q+3DhIy6W~cS)DT5$WzYfS`1@bazNM2M{=PcXy{8!b9DS&-1?TZ`?8NxMT3I z!rptWxz?QX`~A!~X!A$5zI}CBSf7t5q)VJFio)8WglcS70PwxNH9LG!BhV#(Pl%H;B4OXK`r4+7CQqGt=h3$uoX|A?t%U-Mq7s0 z8YR97p!|ts+DOy{f@)VY)4FtR>4U`y>;ifJ%JRZr)C8y@l7n0P{K}2K?v`rFJM2@Ci~)sde_MsC`7|S;YDo zJPhds)F098xGMA+Q=3}RK&kapcHg$)-u(vU5h^MrynEQxBkLKODWwOZoU{p+IMr`B zP|!C4&K)#NofN0Bv6{^l_#At?44~rDbF$#X@liS>JYH&KZf2Z;ly&oT-;ES!*U$ea zGO@c+8>*mHW_;|T^x;{+NV=)l97TyDRL!Fk?ZzF2&D0WReTjcPGy1497c4ogUZhU? z;%G?5CY)GlkMy_r26Zt#PL6g#-~VkLrb*HYiGrgp}c> z2AEen_YHkqX6^J4S;YT#$gE77w5kf4tyV=ZdX0EeNRaXdr9rGVZ_vkJ&A5@h{Mv5M zpP`Q@KP$fh)RIv#GI~0!Gh41xx#E6K`Xrx2+FqF0TFrQw`fD+F`hc&yGy$Co)+0ZJ zp<$iho-Pob;?#5MK1_}x<7S7>BouP;BIDu9_wl)*0^Sz$cS-%~*vNjrkwPlsL=8TZ ztkCM*#35M~)Q}bc?G!tN^W%qbNH7#hP4*u0Qo*!)#yS*T#0H}PJQ1v7I)mR>^eB;6 z(%f7RjY>q4BJ6(;r6mIzeH~IP>=5!L{fUaZ!-v2B0v6axNJu7MNF5};dx>qmk9=9UrBS0qkrbh1bpcZrNr3PsJ0;|~2jo#5Ahnz3~r2AozQTS)} zA!K6fkHOg{5rIl;INM3@Hk1MmF*DfSXfY%z)fl|h3E~@mxDd)pENjbHux&$ zXK*nl6jeYitiLO{$n|$tYO;R8@88~wISnkY>+V4{_ zZ*8#Yo4G1gXm@o^YKF&~eUmYsmwGn0XZoKz*n9r^pWi|ZP?&Y%xdd)(8?6VEs+Y%d zcg>oPnTdcTy`u1$mQvxz>b;j-QD*WBs5C!!5mF(zzy%@nYhs`va4X<{E1Z#B4k|;F zUpdu0J+-B+Km?g~>q4swyQ@nG_2rboxi-Ny91!@gBQLE(G(AF$VehS!vz)gVRS1ZIF8N` zYxGrCH6m#J3e$4_mgvDlM7kt^p0}Zl=}{;j#V9FBDtqO&F>TzuY-+$d)-^E`I<&9i zdS`=IG6D(TUqGlefCI>tXV~>|X>5U@qI#ptQxrd;^oLX|yAi5sSj;>3F`QR)9Zgp0 ztj-;;(y9`JeAp;$i4e6!t%6>8@uRiJp{}uQx*f2Yv@q|~$|BrObnbWX7s4p*BVQQOqDh-+B0OWBsl5Gs2xhae5PVD!u$OG@SgbQedn1nW-ywRE+=s51VDrjY zr%p!B2D_Ly2^~@x^e^z})?zC@mVo(r!F_~}BqaM|V`@_*kv+6x@QDafeo0nhMl$(| zlU%RZ19D)$YbB^Pd60P3qX&=EcZd2Z(+ind$KNuZ7_jkjzBM!PfVgNjZ7;2yJRBU! z2*s(c@l$?WyWXK^jSy2yn%IUR7(WQRmKSV`Ny$rD@Tqf@ZKh+V#Xll0x9r7j0?sYn=wm4{nyldS@%d;Mwr-yw>m2i2Uhv59`JofV?T(AD??k z?vesG`gxq*R;vT4;J2Hjaq1PxL6}Z2|aL^ygISX>cm6F zzPl`^9%({9O#s!cU_{Ed?V$UM#pA8BQJS>VP*>z0^)2b9!;3&hqp;r#2@l{xJ?CY} z=k-*YOUs!Lwe~Z?c-6GwHpdN!uP9=i{^!fuaxjY*zS6Wb#CYF#s6~t%5k8R)Im{46 z?k$HstBpk2z3#brs`;+SCH`s3W{MKWHG?ausmGP#JA=acF~6?q*ofR2Bl&(YKEfCn z6wEkPPj80M4&|(YQlZURpOK2x=^8^dwbzxRa7FPJMdxKBjHTs5j}i|-JuXH>b%HYQ z6PN%m7=QZ!pp5{U-@bd|3n>zwKFDMqXP-wzR5}a6eiwc^36CZ!I#^Vg9uGt+PhVspIJMXs{xwo%FoooM`K*!If_3+Kc|THuweB9@bhL!ke;j zW9|8&+U`-`+$mP(zmgw8+ztf3Gb>~UX_#m6RfHz|`KN-M8v=#9T4g_UP9{p6I~ApwB~FNJihuz-SG$z1t z1y7y(x*nYnO#{J#lSboeJ4!D(lBfPv6oW?et#?_Ibx!MX)6MVZ9Y2>ExZP~a<<@+T zKL6`smL1Ra+WE0Xfe_v+zGLq$26qa%kadh}sY$~+7bs%IZ~Qxp>*GyRqRsXuRBH-~ zrI72L>k9#WYSdaaTO)71bzL9m`u%=EHs8I(13zTfP43Pb4U!1YsQY;3UA3KJA6$?< zil{5Pwqs0T&ODO23rTpd)U#lc1bHZRUE4KM|-`-MJ`UACNGOxt;*o2H}Vq1jgj(i5u$NK~=6-i#&+EL(UHSMHYJ0pOPzH8N(N#Xs1pkLiLqC#d_5ub4RwKb6V!zD?4^9bY#~B#n#nav^X_F&Lg4sAv zhT7{q@2~&Vhh5_rONS4J*^*DJXRNT7EB&(POmPViVA+WFILg$$A^hAGk$JC=AsWG+ zW^wy;!fyH0&B&|WDms1Jv02@CY*}v8m>)p1N33|-`&>Hhjh z%%MYhfIWqZn%mk)We1Z67s3~W+9`JYEj8myd|Lshc~>D=N?=sfKCQE)7XL-q=NGSa zo`vZQ?pTO&1Y5QT|M76tqT+*PfiVMMb`oCSAIvDBl=|l8?=tNA{ao=x|H;$ac1#0# z{ETFV-ejXuzOK+F58+c={9O|Z>$b&A+`}uCB?l0QJb;7$pP+*EA(a^xc4j6 z`VuVDbcoY`R9v{jV-8HO?`L+z(7y?@8Ig1oL^sQLas3^M)@hJp4o#X9ernyx>O|us zpug-PJ1hy^hid6c-NIjR2l5f#)D>+IsR7iu{o?c>-fuN%G6oQ1SKcg$X=ueX!>jIC7J)s!w!!&X3n&Jhm&`fpux5_ zcBi0H$ZhC*6m%E{t1Keq;i#=o%3OElG4k)yV%5iue;yJC%&kNcXQ?~B9R9d?p-XD0 zv-WDFep9&lpNf+`^AHkEF8zx;^%2K44dMj-q+tB74d*$oV_bz)~uWlGt&n{;r`CF^< zKQ)hTS5Dw?Ly~cie)SIlTp>CfqAnqtyyHWexl4dGn{Z#LC4`c10|F3Hcqhx)ce#r~ zk%*^4cc9rT8@GE!@5U=KjhiPvOQrrUT05y`rCsv(HURsS5K+gm29>?J+`L4hD1&q) zK;`bY_TInmUi9v=Idde`*T>1iI-jcBk|RK<%Xq%X_EP<=8a`O-vwXkYbkecLv)&LJ zewOMI%=Ty4fiJNPyom6A3j38VWWT`N6k3yrZ#f@}gK2iXMUdq%p8D+ENmOD_q?LFt-@}+c^^-sh|9{9_S6h-#m=O!rO{uvJKro2(c(?PZIC( zmAQUx9-ItZD}xzL9aFM2yQ?$7wY4TBU|6fJloHdv!A6Cci0rk;2+a$JiVS(jv(YI? zjKm&*5dP8#sKUwdC1>L6pK(~by?lu4_-B#NzFK>|&pXi!OT?LAoL7kOZ53?L(03W> z7rZU}71$MPkStDM^yO8{BJ^BI{1%;O$tw(}&Je!1n`I;07NRmByw{c7T6eV~4!U#l z91+~1R9Nm}QmEV$C+xg;5+vUsvb0!%_P+mg_xq;Azoy(BuVwR5dw}q|o_4nV=Iy1* zK7l66;*UI=({}Hw?A9)&`%+AI*^G3lh~LsiG8XqyhV$?ciE*S>F5eX7-ezS!FVgMgUi%h5%fKW5eVjoTH8ua6%7Fzc zl3@7Pq5;kp^iHwz>#G}ulE;O7iILZKk(RlkARxzjQM)Mi5Kt+mQ`8sC^O_dvQ7{rj zYKMCSo8~_Aq*cwZB^2n2aPBh@jTqePY99I^|8+1#&eO*@da z{&5Vc5HC2xoioY&@zj5ArgAZyN?i8|G0X3w#K(|pqnA5d4EPN8J-gkmB;wRhUlprS zQ)H8}E%i zhJbLt6@4lloAc2o1zTaUW6D$S7Zh-O3O%Kcpm&1Yo52Jv1u7D^`?fC>9+gGaeH!c( zhrE=dssH(v!tXA5J3XEJe0G@YUVe@2UHyvvRuB#Ty#zdaRseGU2|G11yfHF(2dd^d z)b*5vJ9<%M*y8FSc=K#KrWA;SJq7bA)=S=hHr?Ks$41Dcg@eze*mmv6FSmKg2H+JJ zK(KOV{M{*9w5o#MPm2)ILs%gY)djHr1_KT-n}>P#xA3h{_0U6mpI2Acsk(s!EQ~X0fwCqZ{Id!upATw^6YYmj z8&wPXNkr1B9=1L}2HJa8^sso^KipKINf^?5cM<2??61c9op?8#dmA5m%n!0^|9!lJ zp{e$%JRAB|YI-9_YKmJucFVbM3Zl&K-UC-bTX1CCA32OU17V@hiBUz+S199#Se)t9n{2_Pusy#4R`ZKP!#dg9j__9c4TUT#c- zfI3o{z;kc@_Ov_*p`4H{$Rb<_`#r4JUWZ}>J?@ov7e=j#qwp&NvIkad9x#?C`NnW1)vTV@XNNsNLTkYiv znr8)ov68>7KzC)WhA#PXn=_W^p<Tw{8k3^Vb!aweJ?5iDOtJ;9RMKYV$^>6w?iX ze~8jWY0LzZuB8XEUlq63CUQ>J|K~04ZW$nn10)#rap$+~Kb~D1KiRs5h;n2{rw-|F z>^@rJDAWRYIQhV!lJBVD5WAjiZAp|j5#lq1GI(2pYgxuz`x_yK71n8JE&ul=4*2|2 z_vQQWK~y}L2gpY1(#aavUmUO7cNJVuARHg|wUX`FZ4`|_&|RNDIz~2tfL)_u06fQj z_~IuqeE$GoDN#O^KridpdybwGd_1B07G-x#JKAJmOCjXc@f@xV^@fpA2`P=N6Ucg< zwmt2=z+v~k>?!(S6Sk{7XUkR0pMQB*Op+T+XM&Ki% zEPl-$zHjpem-~4$_~CP-WMXZR!k^Q5DHAVWXdLeYiALQlpU_IGowxgh@|wI*{Y=U5 zSA-f*!s-Q3k*W|TFAPY-d_Q#M{kAnV|IQU5?L`qMt-tR`wr_ADtb$%Zoo>{V(-o#% zy@&gOVzgf{&&B7%4~9S$STN}uSdT$@>T+OEHg?z>uXM?E_gg<&KP~ygDIo6?BysC&?XJ%bt8gy?D8-7=RQawIK2hg`$eX(|% za2;=FRFLj}7uH3udyII)I|!T+f({V^V?YHv^Qvd!q1qQMK!1lxY&?d94Au0tA^LEm zd3O|N(sud*LGHLKWrEXx^^6wzn z7M*Q#EwW$u8F=;T@8l9!29-aMls${oy$Z}H-SYOGD|w0(-;6g}KPGr3w+g25sc%G% z2R7uWmt^y5GElkmzYMz$ZRME-wDdEixk>WM7`1wbzD{ZL#9@-(`Ma}_EpEiq9LHKU zwE=?UgI}rgd3my#wJrx9}<*d9hwJeKbVBzJ)buvEoLN7pf_+gm+HpsLFR z!QDmix&iNk$ZdEo7vL2ZWp6y7?_|e-61%1)I?L5b76r@Ejbz$dDiaM*ZuxV_Rd1_w zDR8|Jb0)r>VAJYW5+SE{!?fS?HioWCaVT=i==)Mj76xK}rQGY-1Aar$T;Gj^?Ky}j zd}+G=@D;J5>JD5_!V_Hd9F^xDUHhPe5bWLSVL}?A&^4W9;(g{c0EvB)&U`j&BEYZ+ zj0?(3Z@%h++x59xU-d=?C`X(fS77l6q`>3KH%vHzFY2u@X=1w40OyU|hNU~++a=rl zxw|~uxE>A{xL4`Tu5h1?!f+(mY$(cMVCy9C97{jR#KburomT`lh*J*na2T^Z}1 zr)K%1_j*^}5qIgJxMf&j>r^56zJard3L3CUwR=8yg|(ON%F_#6V(b0l9o!@U?W~9p zk>}G;*L2%s5;|eLnePIU`cbLOAio!qx}^V-BAmi00=g5PnFL#*ZSA(cCbo+n@9eN) zdUaN?B}-Adpe#%A%f1Q|uf=-A<)W2uoHn3oc@U@pfXhk0>TtXbsm|WqfBLsHcFVTEMm>NeWo)DO%Jt}_@ zkuK;WE`%rIEpgR&ebn7R$~=&c=wiVi_8tY_EtGpX$p}rqp22VPrb21sMY|_kZ9qDX z=CPfvyix9hdaQnrp6$Npq`0@b@@81ixK}@2HWDzDxW{5{UNE~~_NL9B{J#sRAH=_@NtT6UswB^3FdMoXEEP&u7zx;uy#I@9D2+^)R>#P3U)mLr18(nQMn#JLAcf(;a zlv0g@`;A!y`Q`R(p+on0x`GX~t!d2G(Ah$M4Cr3F0mfCGv#B#zWy>>*PweN#r;?~G ztwLVYlN)E~yXlS?6PbN>h4Xw*Kz4naQ0C=5Ep#$-vQ@iA&}3;mK7DyDSc)@`XF(H# zPCumlF)N@avoGSw%C^uwbz}BRvM%|o)M`?14X)nV7sB++9e<6op&hu#{M3nCf??Rg z?{`S+N`XFbr4E@UB6fLUz-vD%33QD807-Krj%bdJz8qlI1|;+ zdJJdNm`lTCY?`JlCHlhm+@L%V@A+innk^WV;~Cb9+}6XDp={Y}-Ra~QW0Dv@!R zQ1NQ0^Q}V388*CzHm{w)X{_V18J{tqX;?@XX+g$}9^)$l;Lt@j<28i{EDiq=`ymhF z>Q!nYkFgA@S^}sPKS}3X|5*XWK?)eV+If%GVnH*XqBb(Xw0YHHih>d2huMg?vEYJb z0k4{e2?PH;%R}=FzB21d=l~cLLRt&`ZFs!fPqqNtKG+N()Yt;M&0My?kW#uEL z#N?h#d}?$qHfRnQ@+vmd#?s|$k!o|}d1e=0GU|M&i?AIq(2u@K4$kt@@k>G&W=4h> z7mB8`$BkwE5XDqg0wmv$&UJ`MrWAYa9tUV-#>x5!yNOYuFboKjmpZOo9BvJ# zuwZEhg-qen*8K3vQX;34c@x}VL8S`)uuXWW^gpVoD*T%q+?zz(PVa9ITvE%-#7GR& z$)3BJwvBvZzxlGXUDPtH8a0`?bcdb-UsS1*IoH5BAl)NAyLxU-^Aw{7^3D9YDJh-3 zu7Of`_fItvLkgUq1vV3c9mEq8C?V$R{7!QHj$_0WA^A8|Pw8oI)S}P69mI?t#;lL6 zjmU8;5+yzL-;Eb7mW#uP+n>6|?52Sk7$7IsxC{bOl5l z!%#FwwbxmE1or=7y#ETANM+vLc@7#pX^I0>W3-$avLDt_p=gDAx(>RrP|im@Ds1)f zHuN&{0W74E!REUBpUS~9QBT+9br*P;Je!B;9N8S@$TfO-MJ{s9HyK!U*;)E$eA}IQ zKi1ll(!rDfO~@Qq?ZTt7a>ciAk=1B&K$U&*FY`l%(%PMB!26% zcvm#Z7Y%0h$0t|pQ?*^cI<=da>M5G85TQbWt=Pp;wOf-v_Ma2X#=9h% z2>vC{>4h!Ys2;_l#*n8##89X4@i5;4W%OvAFa+bA^hF^-Z)sD3(2x@6|B;*GbH~wA zljRlX^3^DDI#u@7CbT|F+6dm&NiK~KT-g1U+Qa-PNNpZdnRo9auu9*|DvgIde~{MJ zJoD7`s9g&T!4Jq9%CtKnH2THX4)TM@bz}(=S2hbkQSu+px-VDEAczza^D`}I>Lnkf#}?;p*Yi}t zkJo~JMtjvp<`#@k^M)DN-e%YltWK+RVK33+-ZX<|L*3ZiLZknLIr;g|*%F%|=7}y|A=3l`9pIw{+4;ao&DsOb;lq;!Vn} z&9oa*l6{m7sIr;}wRwl%RnCTkUilT_hyae)y2EK@m5nz4eZeB`hu2J+{kvw+eYm!T z*&p#RY70>D|NU_!a`PWeK#hr~@3dZi3!}Z^qdl1ks}MVW1QhVn>1zUw=@oUYbpe?< z>Vs>BSE44kt-$W>_KHi~x^6A#EA%}u05wOsRh(lOw|l>|KEKDWNJY09D}B7rYLE5m za|pde`15*blI}KLtf=Ntj~y$ZeoF@j?(gF~PgkoYdfAs4IxQ{s8o&uCvQyZ)=VR5} zHgE4WDXz@<(A&544n_%u?pajnTWKeL1J;}$1s{0v( zSTbaFQdu^rppuMv17Au#-@A14)(O{+&hi8EoCuBLD~Nhj3|aq92!r5I>~#hvH&u7= zTQMUgvjVBz4XP1IcxA*cS<2F`=piZ}(ViPRUCZ3z+AII93!MKgh1gK`_Qa0QZ^sG! za#v$Y@x_^eR?K&>HLo&bjD2|Vb52luc1-A-uIg zY-OekS+4^67=T8BX#`O8BTOZw`AG-K7>ktMRf50iugfFg=mHU$gznCyQ30i+e_pXc z{6~cnf4OfN=6j$2;5%YVjE8lU`^n^PSM=;0;f!C@AMXF!NCa@pfV;}n0ArLXF3Bbr zzakk0fZpot>NyOk>mLtyRRg+2W03ENC`z)8zykX%6)8eoSbE#VSx>^FjE0R~$J#bl4iN zCyQnEu9P^>7x?tT{;DDWSr(||yav=N!^V6usac0xE=Z5zmF3l18fyR1v~r4+2ycSL zTY-$#2~X5M1 zQ(rg7OeI{Lj~9@DTUMFc(K!7zyj1^Yr?g+c$d6$J6>z9l?@g%kH7!o7?IYa@ zVxDQ}-e11)I)&V<`<*x`tZ;(& zyJ7?Wd4;7Yl9pvv@+gl9Auo4G*d1WlzIcZ)TAQe#4A|*zl9zr)XsYnMCHl@^|63Q1-gS!Ue%1(8Q+m)kYXCX##^pzG7TN zk-`SZ^Z%p>V9FLnjMiUWbfgQ|C%#%#yj{sJw zgGm@tA(%X9=Rs8jh}sEE?Q&=UWGh_&9)+km1sOU1C1C6bW*Kikv2*iAW-6i0Wr0*q z556zck~n*j+zu|#oWkobpGZG+U3?|blI1WlPHyT`Qo$iYmRurazqzL zdkH}lMR`l6uuW`zxl#Q{V8_q)O!1)nwbaeZr94IX6<{(iB*o|U5iq>4+?FZehKs^0 zS*pL6(T{+P-z!K?lzyg!>HZuDNP3H?nt$r)I_3Q2jk2PXIn4Geypdtb`cz2l z!QYk zJp;yP4~{(2%dWtUhirco1nooUs)y&hMjyPIhAX-}IQhT+y5dmT9n%&^2~P6q0r=^!A$^|34I z2wqG{Y8CXWeaa!8p-2$?^Sy-e{K&&gr9Tf~vo{jb@dhDbky2n=cVPm5aoj*BuN`$a z27sOX6y&g`1P_cYmHU`cbEHQ9(d z;eG34A|fmyWQ+7K{2qMUdYcj|c`$^FF#azw^&i9YxrjGk(1TCCeIU)m1>?(AeJJ~l ze-Lf}`LLFf~IFt!Vq2ikcKpN;oA-3sIg zZ)Tg%+TMafP|FmD2IJO9K@GdD-GVI{bm6@`U^&im zIwxa8%5E(=k@?Y^mWrm9sPKAWT+y3w9vB75_-Fm(WCSdC)#zl(P)2kufgk|Pf1(HevU<3RmC>tk5&mBf#IiEUOcm!5MwN|FfWD~G>X zelGas;v~gW0Qs}Lws)$r)?4smP>`G&4Yj|xeG#o%dWnvhlf~O7aXm5nl%YA?;j-mJ zUq>AaW+;RS*5A74w7)^$t!UQ!e0W^Px<|+66yWbHLt~|wozCF zcO+Bg*(rm`T=U`&EKKK6y^UdchwWiSbo_R4LERCSPp$k+z9H9L{qgPMchMZ)s~sFz zYl)%+lI~yMT2VaM!wqWL$G2ybv|17=JEZsmmxgQKUMFJu)a;?&T6Rg$Dm*FPh(Yqn zoGt48sg=qs09=n~i)+F+ZeJ;bv-SWGniVV(!9!`BV*W3v1py4!(Ng&`3E_9Tr^`&> zTj1F3w(MA#UiR#@+Q8#w9_c%sR@HJ)%QTxOP{|G#?(#~1I2RY>5}2dHyE(Ut{4?|@ zk`KO?Ia|N0>v^hly!BBa2@~&HfdKp$gqrmigc=p$jrSLXI`bEVYC)&F0f109{sTh& zz+#{A{{upG28fFf;2|cRU4EUaUrc!`r&Mb5N4}40bJUXxtn|K#pi~TyWH=abkkMbz zOslB~OlO5ipR+D<7CDEbh=A_!mk9}D+R@~-JMR^WH`jM|yxs`LTY>7$w%zxdAZFMc2?`)VXuWonnO%e}>9!2B#HiYriC^ZNWF;V+>+anVe6-XQ^k*y}~ zO-WG{h^6Tg7``RzWV7)Z`bw|3&IExkSlNS+M)heMdtm4;^*+n>!p!jLa#@mTp=V6` zE8zAZpHCY=G35YtM_lkz+jtlMryUmU*ew=qDQ{tM?>pZ=W;6p|bz6EU&i?_B3^;#5 z@bAwm6R5zus%h@CL;&Upunzq+x1zqCoARk1)H#N+=2b;Gfv6`7nK>-49Si^bvke}F zeC=8$3NbXk4#2I&n4!o|vMLHY@KZLKXb2p>oPgwGB|kF}+u8Y0D7~EKdG=-)36eJV zltA@${8^4TkN+}YdVXG{UJU_(_D(IQb<?)5iM)|bnC`s6$F8n;yzG-a)H%ax(=0SN3%dedLj1VFKXka+l_CF85}kggZ) z!NQ-j9ejtIecC1aGOpI?^gHcfh%Qt)@FB`q#s8(=nugH1qizJ+qQ|w?XEvgKx`f4f za#f{4iYyHc3jW=QWyN{!W#R-edxnh`s&ThxeA7i#;sY(tIpN0hHBu1IC`-T9YzXgy z=EOE#PD9&@WuDO3@JcaGI6m)6HNuriv9#ZxRi_epW5a^>wZ+()lbYnyo}&=EpE%=# zreaPUs>)$=nS32Uoi|EQDTx2!lee7Cc{mY0vY321(bS-t0DOjs48Mv&=DgQ`3|92z z+Pl#qtuwI7DDuR>i+HB1gpaOa{Djj(^0xa3GzVl-x4H})X2L(O;7T&jrxXqQW=BO#6dhBwN?@WWr9CNoGu z_EuN?xv8@s^CB+PLgYiA+wJL|y@N(+%&olWIh|Gl^h3U6R;C#A_cHc!zO&Qnp$ZfQ zns$&*;6GOjmt8ClVU5N#`oaBS`$L=w+7vH!`+;m%>v;PNGrr1>k5nc)1USCXKh69? z5=6kC`~yz4yjmPn4NjcbEV4Zd7F9xz@uublbby$P_Gic|^s zrq}oMs`@ZQc=Mt%SSx3up@{PWHp!IL$kl+tVI6|uyJPf92sAQM*-BN1<=)ar1})tuV#RcSER4*1HEEJh63Lz159M2}gFJK>I;$&7V~L>4#7EB+d~Z0_)BH2`Ti(r8gy zqHKI^LiBLJ&2r7`i~~Yjqpq-b55LAM&;7p-?Atz!B65E$cy{u1kfUlJPp z9Lv-s8Ike5Lyom~M}q7kt%^#;sg6-5fuZ-Nq`Siv#HVh7-b^x!CNBjo;KjJfk39Gi z1jpFQum}a!=hn_Qlomb)$7ex6&6q0Vb-?*jmH4cxyp)i5cO1J~^%A>((>>$gT{tqP zL#)$!IQrjctJ+9RQe?v&LMNp=0URZKg{{eFG4Ik|QAv3YrKdvY5VFOdvGKI-4 z*E(XVx)B3c7VulLBGQCL4Fbc&@DbV@rdEqg(NZeJdmUH-K#I%EHU{K`-&XC4vC{)~ z_Eq*gbBpB^u8X>NH znW><&71NWbui(!^ojZn8hQSq*h6jflWR(`~KUOauf@z0Dty_04{E<_bq;DY{uT|_;W?3L&ga3&`f_1 z#%?L#&}2TJX+$leiXB{DvzX5Zzngz)AL{oMtS-wCelbQk223Rjck!{`H?~IZ%oBVA zM=!3!<}vR*;xxFjM(UQdC+Em5v*HHyuQj&*hmS$11?ttn!dH}hh}#Llw{$y3p*f;) z>mR4f=lB1&^^`V$_&SA4vtEC!7)Cfd{WCA}p)fqp#aB$LlPII;6n}tl>CUT_+GS(q zVTSm<7=6F$ELf;yR&AR4Nr`M_LGnGXc16i1S~8nIFcB?kos?srKRpC?l&mtc<{M6Wk*4PZOWSA!pepzKfRfWOjI8TS3zWpAke7qr<@_&`14 z83v(@=)&A^!46x-y_AwSNlrtU+5bkdy?&j?@uz@q5*$}5pT|aKNT?QYkW_|WXZ?gm zg6P;RlbFz@lz5h=^HoLfed^*7*FM)cw*Tm*JN1L;?LU>v zju^BbpB=vtuFLasG#zj*5t0=hopX80dN)i%S1cH~e@BSbFzLeEKzkw+4V1WS@a1MN zNaHDrkQBGqeY2xLEJ@B{r6JY9+Lt-S0S3e$yeA(7V$H4_DaHtJ9Fr0qatA>JFaXW;o`wKa)t8dKMJioXYD*yTm5ynY`=>&J_yVTwraAE9L#_JV*@DVYP-vV`)mGYaIC85NrhRWU9CGO_Y0Rxw`q=t z39WMUmGfYVMHT_zEdV3>26C*bI9{b}>r_|^*&+Ware>*4YdZc7g1*_i{W zLg^QFG;@kVW!0=(+e_ytyTRq2aW~)lLqxshU}^wdnVA*k==CCnY3FF0@db?Qqzvc2r+tOXi5CBQ~n;d^uNqN)R5PUYFITUxe$PZJ7ruCR*Y%SHrKj`|&JTrF} z;palkdm*nsF3WM&R~L0!FSiEL6F&VOzE4pWc4UJhot%c7JG{0ujtu0xgD5yVH z*qu90PZ)9?*Xa>mx~lO0f9QJia46%qZM+gHTcs=sZAgVg*=G`^tffU+lO&0;k8Nf` zNVY_lY-Me+lXVtL2rhYfYhi#Qcaw)u}yQolBR`Eg;xnTqKm(4#l*1_^a{l z5_6)r>*rRlp;?dhWV*L^;K{d=)=*&9Be#?MjVnOrwwOVsKw|fN*vrN|X>GY3#?bMu zF&3qy{#*Qp+MQ_AEIC^%bTz?HuxLU|N2=!}PA)rb4OwHHsaM{31vU>{A{-2un?fTw zK#NqeuC!LYgYD^wI(_}Zb3mq~^8GfjzszlrC?keP$f4-35_A&XUZ);gJB2xUn|$zF z()ACHiI=yZ|9+=ll9Jq+U9NZcZHm;$(hMSORMO%jP!D9I*$71`n*7;Rw#}LK4_w}3 zLBOODKmr;&fA)6*aY)IsTm452&WzKn(t7mW4QU`k!JKt(_&My6Qa|&ErHaP&V%m;V z$d4f9RnDwZNfw4~P)!eO^C2r^INP;P=@WS5&gR#eRdJbz-=ldta0;Kic{*$fKf4B> zO(t*mzEq$3L&vDC`Z5Y|xWC^g9Ax#1`Yw)wISWo{Lz6Hq_h9 zBLmN`nd_|o^dDb#%z3cZ4EZS5bzD@Atp7UHz}ng48)I5RH)H9mSFk-m+W&(z+rj^X z(hv6PzpwOKJUxF6K*~9FeV9Ha-r?>S=Q&%u-u}nK`O_@mYP@omdi{{o&q02vBgi)c zX}-08zkgK!54T;DS?2R!sUjbPHnm#I%)9KG_7&G_mOd5Mgb{E$w-;4J&Tm^(NZw6^ zZD!%3F7Cfh?SFq4#29v8FcK}^znI;)v6*h$HNfc-?BsU!I_3^dz{Iv-UjilXiX!@B$vqW07TfuA zz4-6kwbQkg>hgJc*Dy01Vw6{LV6OhI*BY zk+wy~&5g<1XP}v%#Y$LS%@!erv`xfD7kN-A`<(>NF2%cfuF$so($EmDiN%Z#t5$^92RPJVrP=laW=s&@7OIQ^LAWtxZhpi->AX~nZ?n$6=~ zo`A{{udgqrqcjcH?pFYDbu)XS5Zk!AqJw zJ<*Yxvly%>AKKl!5&6sq$68zfgH?#3Sm2aQJfAj$#_wY4#avba+7Sd30|_x}T8Ngp z<eXS~-bEqA*Ylbu%T_+h@!>FP1h z{pBJ8D#gFcB+^SQ$vS8 zu{gP19u*cQkC&p}X@xN$IL9O%eHDtG`BZPFaMPAl1#3VM@0c{N{#^aKKk`VMOSKOt z#P;S61A@-U9gQHV!&F{|&gF(~Xxw{_nD}Wc{G#Qv)&9eVexJmTE-B=9`o;}AjmWoyPZk?YBI1;C9P5!kqlcTZ}DtiDWu)h0m(fddILTC3w3 zA-)f#b`fCq(*DP9_;vRA%|^ey|0WxT1J9cJYibT_6+N!WRZNQBCd<047<$G(7|xmR zGZV~aTv)emBI_*_i+E6X|&X(F%b_9PML=s- z5bSk#a(VN>la2`2-o@4H4b%uBL39WI8{l>;K5bPa*gFZ`R3S4dFfC8}80@|&m?x^U zsJYZK#(>~BNgik@%sPH?NGUaQYft5Xr5rd=>1Kd31v3JuRU zQ-?8$lhlhX1fF>TYbBAK~-^R79*&qU(;mYuD+TYkSfU_Y(aTCIq|E8W;z zLUX$@GC-;!c!wKY+d{1j{dFPH$i=lFF8YSD@fA~VBJ9n}FW%0st!+`{bu2>8VEKCE z)U+9HjJ3WS>Fs$2$jfZl>9Q%rusUptYPf6^IUftZG9ir7_?3`u|524ebHhnq%`V zj7Z2TbDPL@U?w@i?lw(D2;mg9Cv&3W^s6HaqeaL4R(}96pZ%AX0ObWY&Cc+YCJwMr zfRhJ#N57s!jUihzIE?vbVoE=k12*^YLM2Ear@X>vF0XVFs`(~#Id3Q#}IhGvsrEO){BilFfIV%#@ z&+TrLul(q^#e4tvJAN_=k$pB66#NX&+~TkftZ2HG)IRUn@Ly3w-b+m_=g!yT9EKfD zpfq)?bmXZ^$mhhDoc(bulda$a$nz=^LOjP{naaJANN5m5f+MLhDLlk zT&(zhY1_UC6Q;MaF016!r3@aDs{Su4@ZjS#~eEEvDTmg<3bT?6G7Y^nqL9`v8zr-7DlJw2Qb9EunD!2 zX84Bq9cx?ku%kQ0CFz!)y#MX>ulKaFL%no2Eg+V3d%uJU*d9Llu#N8b$v9hvEOH0A z`P}C={kcY0JtXrAKJt+O;=21Jq2_wyCbv%)% zu)hv1q_(@5WawWLRc#_^JUkMXc)+rhta8eL^h?_iv?d8Zsp8lHEqTo4=h1mMhjW@= zSA#K8s>X|xbFjU9p86ngs!RAv=xnn4V0DrE)N}_et;hx5KbhvgnS?OMDz@sK_Yc*+ zAy=~-GA#L%&HOI8&t2`O+xNsc%h2P#``q`*$=I8JZib$bmMljz(DcoTb1j2lsIBhY zD&|8jH+r~43Bydsbz^p9$VG_twc4%%x$Em99>c{D4hM`gvBukVkpgCJvqN3G*wa`x zxJhF#c!w+vVg8_H*JD(27zBu!+D;1#pW?`CLbilMx3i(lu0_$q%*-Tj%Z&pgr6Pn&A1%lkbN5x`VuW~VcV9? zvcY>te~A#;fmOT6^|1?sS?CUV`NWmLKQknHkQ}GE7xS2p>XHz(d^=IS|E3G%M|v6e ziagtp#P?!x+YkARvh$nvuLIomPw(+&!8jyHGW&Fiuk(K<*Z(Id!~9ZNM$lpWP1(@+ zV~b-;3HT+{KbE16J~=z{dQ%{S?P(texmDFHnEJh6|Gd==y6*g9t@n;^Q~O@x^z_&I z0A64Hn~n>U!LA?cy$xNgbr_e+U+u4~Uds)2cA`Sllp0|M>#OE*Hsv?1SaVu$-sApK zk3qPDbF|+_*#`0rQg=RQicKI8P1DEW%ZJfi&QuUd9D{tbSYiTGx-9ySg; zl1F(nm=ahb=-euj2nP@rKPl0gd2Vw#%5e|M^Sc#5NA7?2yjsK}yx`<0dr(m6lGexKKyoWuZjn<(8d11olRZRr`pqJYh*XCOw%Hy!@&cy}cEd-U&A`0I5W~gaZ+A*V?jm z{K#9fKGQYIiEJ9ckQ!-|0fM(K#%P`4b=$;_zfee}^!e0HH66z}CjSQmQP9H+^SN1& zDOl7@oEVnVkKyQ%8Jlonq$)DyDbR@DZhkXa{XIfu!SJ&V$Iznttd*1pEg5Ekk8;vhkZ(KTEAA!5uFb3GMwK1-yJRULpPQe( z3PB!DoX)wD-dJBE%U*FSQfOW?g{(hnVtKi*FXfO6KCK&enUSC?m6H14G62N%bm&=@ z@ATNikVeIO5)GI*YuduT5#Sl!k9ql2Q9 z+xeQm5_hTc5=W6+u`d}|VR-n=zfjXr;s0}nb+x9`v(>u(CwM~dyZ5HlWu?q9Ks@0R zf2TE;_qinZoTDk7806B#`j+Rf7^CIn?cBsF0V{-dpjpK^V!8v9(;Cm8?E7pX)6)!w zwsl13%n!WwUF7zRWW=R}^5=aG$JD^(Spx0oO^FNUR`6Ai>$k|oW{3>02F8<+t1p-7 z_Axz1%&L=|eC%im2g51ypqrW`CJMK-ZCBuax{`PmOcCRb+ z^#1;&SNgQYJ<0i6-9;<&y?#_isnh1GvuzR#dQ#l?0e}fl2%g#pekKj>2_c^g{ywA+ zN9oRw?+7hsD)sO|CvQOpTRX<4-4s@!zX#nrK%qrPLsxRsN+&bn<%~2{rfjBG5{MbX zQv%(oo}zCK*}3}&NVeL}kl~E&#Pb!uq4%aDIvq`9KS)rJ8Y;O14yUL!(|ZWn;5^4+ zLo6tK<51Y*1=ZK`3_mJJ=%qZs2}INjV3XKDvAR)o?6UGQXoM z896lkwG#7PJkCno$SPGh8rl$rXU4*Mby z^OXZ@x$yL((kawH;EjjzQsJ%_lP3rk4=Y&AVSMM0jpO^WW!oq}k8)dUtXc%Is-L@0 zS_awaeLh^*OCO1pb)ISZ@gz7Ja~PH-5CPSs7Ro5L=Knn%BcdHUO zX9#dXwTO^ZaZr3A10FH}Sjwigwzyw;n4mjj3+hS5F%_y08}=R620{o$^|v!>Lx`|n zCaPeTjC;E+cjR&5k6o8UNA8=+*FHQYts{AkC^$o(^!*+rjFtmQ>Sudl=5-}@k3+*A z{HBArv9tsXhei8~+3{wM#WZAF%$+6|$EhZ-q{=m{2HO7mw0uFQ{sb*#;8j&x04WtW(q&D5@UGn9DSrseY5PvKBJ>DmF+kiVoWP{7M}-P#sP zH*m){Z6P!ExJ(o9qQPr^3LkN+K!-j=^n;E~PcHfKwubR-XD-P*68mFhZ$N$fx*_ZC z?a5y`&2rx<_pWH30;H(d;rnRqudaPB2WE~nM>xHAxmI@xUr)IF{HKK6Wf`dfioLtmwYaELOSDOg3gJe_Mf7X#-i|YgE!klajKTfYfW*vw<0j&j1&DbXpZdLsYo|E zXici=JpZcB=~4kwJIH{M+{q5SL=T;hzJssNV13fB;cKlAC)Jqt)Jt97jyj_~bkVTW zN;ZV6M>M?Tix)#=z?l=2!M0?0YLyZMBNbTU-~Gtxpk5IyQ9FV;9qH!tKAsiJ%l%Xw z%S`5$fxrzX$`HH23WAi!q-D|Lwq_#SXd2=}Z6_5?F_gFmf?I7pD2+0cz&n^x3CU}m zQLM2AdD717Ryue15jm8%AP!I=ObGC6V=zB#!3q2k?z;w6kWs`@HQM;4xDOX!6zr0# z(b=5GKd})sk6~P*Z>xswT1~THY!_^dYH(Ik*J5A~B-$x`$S5JQ=rPQf&k}XDeG`E$ zyIb_`J~zBsepDT;1rJHq|0Cr@#G;{cU>km4>P%h*=Pm3lw%kO{5c>1CT^ocia`%~2 z@!Qur3t4)XdIB|Fb)n?{|7?@GzMm<_7cZ8IS&GtOJ$G?!AM|y8E?gZCTBGBN13cV) zAL<8fFBae<0H*I+#_^?+^;&gU2PhCKP`%yX%vcl7$+n&t!M<1t^=dFIHBNB|0mUNw zo-W4i_4aYe>oQQ4^+!%bZKCklPYV?<+md$6?Xk3453*0HUIsHKd zJscRhjw!+*R%LKRl<0w41TWg@mQBy8Bfh-V`jTa}#xtHj1qa)}HHQL&cOeV&onVYN z*waGifWaGEc6e+;iSa-ttnz}&gT7Jave1^vJ+Gn3D}hu)ce;`2tIiGjBn zh9}(0Sq%`ogYUt}DJg0*bigmS9pv2`B{|@CpSpH*e16M`*JNxx#s^>UR_CWYG384% z65`n^x}JlTM4CKRkNQMB^#2TA5&WF}Ctk}ynRUQLMBQ>Dh;^f7_8_skhNqEt@|vj@ zoMlH#{-Ldd+yH}>&Vhp@x*q+zo8_hkg=}Sf9NDIhymSPo6N8R&$xP*esB=ey_f?y%N5-vu5Z&Kps2e& zlw$lRB#B;$)W|u&zjYVEqvKq1{hruXZ}zh#X_$e1!C5f0f!~Dd~ zViF-B$l1-CciAh8@$y3m&zltk;ss9^`D?L7^@9X4MP#%`g&9z##mRBd357D<*@Ge9 zlVER?+^F!>_+-LOSvRwI4ziQtp$`cD|9g4vWtaJt-5-0-JbRDU>&rzila(&dDj9{SbD zxSQ7x?IPvu36)!y`bfq5-3{3;;8#Vjg#MX%{=UMVF^a`0p@pM~ZGOpi58GlY*95xw zB;^Ca43cH5)vwmTsLGFKg%k6PY-5O#xn5Ev%J}!6efH@lz7|0|`u8QBHw$|*- zI5_)0`aKZiJ)*pAo)!aWtF^MBAk4&dqVCDs#EH%rIn#75ICR(AZmY08s6MydpkY3` zOLo2GRn(+23wHJJn~{;Dnk1C_X$ER)$(}QqzJr6bke8x>0D3k5wP{hkGP@>;o4}~ z_)Z_8L%BZ~!bg}l1GS|RI3@MJ!+84Z)c({QvZoOr<)$y`$-r9_{jgdQ%L}P&@CJLe zbzx}VmQ+Ju7KwiM5%NIY{tCnTTe6Ss*%Nw<*_4NTUMOmQt#cnAnOT7_N=*#}=v=sC zPo)*=*oXOpDG7Qap&Ez(*EaEovWv?D7UtDo$%}E~24RUmTa`veR==bM19;jeybl~y-(mzChfljO=0gQLDH;_h&EAEabUtQ;6PPrZiM*6VDAhZa|O z9NZpj?BM5Pf0-BK;5_guHlq&8NeH0By~G}QY`?1|Z#X+=Q9L#+BKoC02IRo~0enp5 z&cgO*T9b|x2da+wJs%7YP_7;vf=_z4Mv=Rmn7(6g^3w1@gket``};_gM^cymM>vl< zs?3FYA8eKl)P^xre79`SrWKL7gqgaMf{FcIGtrc6vnyQm>eg!)JhG$GB8-qiP#hD? zV;{#ILZ%2w-jjPLxLvMK+7%La2*@+dvG=}UyZEL6_3`~znrzm7t^c*QYpy5WXY~b7 zv&!>6%>IJ6>`O6X`yY1279{2;RX9`%gOguVOv%K^b9)*Y>%Y2qJu=r@l1v=)&7NJ6D>f0>u(?CEO!aodsL zF&Tj8dw8N92peSiktJWY@`F2K);b@e{A>mIJypzX%BKU5rn~A@m?lA?7stKHMecnQ z(2_o}?ufn5CtzK}ml?fyxk$+aizz8oAdIdKO7y?W90Rjmm?SmisR>5A{2kE<`8oEY z8|btO!8O2kFWAjt-=oSPA6;@8B)K$Ib*DnD@H~2_k2VnL;l6usl7$2HbO{8>z-_da z8Dv8{yYNunR)ISo9Ct|7`!Og!Jns~36v{4tS_FJC@)nTO`FaHsEtIn>gp1z1^@4Zu zbRo5)3PpLjoGt%3L=Ha&%3dedA}S3oK<$05GkYi~vrFqbz^AQ(o^w0Kyc9&LcuAc=VcItM+p(o3yBc3*vA`ix8-YdzHl zW14!{;&0$4GX^JyVIN($)}>|?kk>@mH=l-ihG^YbC>Q>70#5siP0zh3VNOE$Z)@SA zx7c@@8W;n+wJz{G<)>XsuK@M4TLkeYye>#l5|W!hHmZObF_&%F)*)RpiMJ@hlR~(3 z?tHs@ihks?*6HiyFAKTvb9b7Ci$63|M7yBFnoet=d<&qdlI27!luU@uHhWwtwM*wN z8&E9rctncjLN2$;f2$soQogfBUvw$PvcSA#E=3Qn#rfNfHREkCo$9Pi&p8&XYH)057>yOLiGvEjwfL5mw?d!G<6voJbl!bkaX4jYya&6lt+`4xY^Vn4V$UzG9UfEU z90bn4&fdhFh`iTPq~#vSdbkhuq3F-3zsGSdW6E_}2{K9lcjbGZK;@4T(9A_9dQ}tZ zs5=|Hy)F3^fW#&D1!eb&j~;bjyM~r|diH&OX2Y*nh|h?A3eRz!ePnN*lTU9E*c{BD zwsAPuovhkCKzQl>{&)O3vo>n@l{|;_(}^8b`}BiF zwEiYoG>AR(Xm2@V8@f2E&Rj`E z>sgro_!B^9O5FECF_p;C^4zly(7}#-UcOMqSwVT|%aB$AF)a}`lSCM(_!dmQ1@b6|oqmi=-jNf_mL};-Pc;_p7NL7Eb3$^sYJ*i#H$}G^a1I(Jv z5EG!wQ@-hLp(wY$1I^%{RCpk8=U!tt=q;~OZW7sX7(m0q)bLL~QvC$bouo`M=WCx3 zVK`u5;cmD$@BC|q3Z)jGU8@FiDQ3#r!T`SM6f*RHr1M5Tagl}G%4~$i{jV3N(HZvi zyZQI=8_#iSq}fEp-h}Mm$t{ZM;bmw zr1ZL>SGDj$yd@a9rNrRFq~f3;T=-va@kEylh3m=Edz{SmDD_UZRl|;zYQE{aE-hZ3 z*3f?nzR>rm63ht9PDis4C9ch59wU5;Z4Cnv;;z3tqh8sSBO@Y#c{Thn3g)(M9VXGq zo-l4${~lI6%*(vth$df5rK+%OYyKRg~lok zZiQ0|xo}-haTtT)(k(B@QD`S1fkfYJ98HX{#_7 zN>{xFl(l?J;9^1$2r%|%!Hk4)g&v#t~e&?mEo#(jlgninQpt9^+; zf(cvp+F5btRM+kqMiwwbU3QjPrl7zR0ex2ZvF5dY>`=@2^^0GfsupsAf53ax`s09V zXMNCxN7?f|KhwH?LPo{H66llZYp5MJ|CrYNx%0Rs&h}gqzJuf4Xn$G^pCj%;X;La{ z+!Sq6&CqaDVrmorvQ|WRa%qFh6N~{Stp$L~EC}Xwk%}1JE`d5#13a|AiaVWYtpPG$ zW&(aGO3k1N-th!-8HjOv*kOBANu{%6*PDvQg?6_B9`-qogMH*i7KV(^K>?Y$?MjjbN-c}l8#R9?k`KvsK+<(i1~Q#kUd+`BAR zY}8hJR(&DxffsFgkf3~>PN7&7dA_&eri>OzN!(|$pW)}!30)y6?ObSG=w3md>&wEN zFc2nm<*kp(GjHnJrukAEyV^sI-`!*e`)dYb{5G?F@j`< zOL>aab$4x@WTt)wuxm=7KiR8!pu}Lx;U1%Xz+N8|WKP}DUVh9Qo`52nHha#0=0(@y zk&W(ucMF~gt5n&x+XWW4U(6CyQ5hJ(UQH4ZNncg5yHb*crlV*-pvaUkw${cUVyK&K zz^_(!Wp?6k2hz4t=ezd%&mYFVY}z+cTRR@{kTK%>ik;S(Rx5%kf^A-5a0EMbDw0yA z{)00$jQ-;x@cTkl2Cf*Q9;MjFw(u7E2JB7b(?{i0(ry2*N*a$6;E7H1U|GWz1^X2& z4=$)YdOuHg+9?v>F6I~eHM+NPL=Yi1uNE;Lr9B!=+;3TAKkdvw1hfmlAFmeoY3}Y6 zY@UvM;W@?seLPa`(pv%vE@mhszR|bsuFBZ29;CI~OB~!IrPROxFeK|aq2y$D{td^O z=l~BukE7ppSksJc)J@0$f4HjA4RCTS6Tw)Cii6e>Q{Agf~2B9MuLq-Ahz1CJn@nDeI%Xr zuB6rG-xm2Rv5)lQ2wK#hJyPp@`lvoA^@JEbhmV!e^-kjX{CWw8l(F@s$Hg+lGGqhL z3xZ66Fxn&!_oag&1tBal|V+ zjEy3^!9ZoS1}MUCeGRS`HXI=tq)**lf8|Ew!E0$!BP0)Ar#u(y&(Z2X>52WPHRlxB z019@w42iNy=}uB!g?u~RM&x|5g=8x&E&4-zcyZnF6PPz5-L7%x?;)>rKQRqe1_s5z zr3U;w&T?*H3n=zzcRN&u>41B~xzqhZ%3~^Y0TH5NB&53)(?8>I(%o+bC{gP^5);sLn4W+Igez#oj zl&ILUeG#I*(uMomJRqyg_}I(weiJXGrA>AMWX}`H5sFS}Y`h+yyj|0@6mqsuhL0!Tx`6SgVN%@B+VU>TL)`!Qr`cXZvYYY4x z8X{ex2F%E+m{9{rBf!+*!CIq@ZMdr#r?Kok|BsD@p*_)s>g#tSsh;}1P(lKn+dBn) z(yjWP+@H!^uzGCL!YnV$7von=wt3AG>&jmH74DJ%xnpOF2?b_VQei;Eg}PO}`kO5v zj>$`x-VBf>tQmQ%SgylL-tt}RMBOl%PVGj5KqP|s-&D|Ejkcf^r9Q9ih}e^sYqi4J zXA?8B>b3eu-qKoQ)hwHQ+Tv3}&plo?9#7Kp|FfHwOuuiNw1$~XxDJyYal5Ur@l-HC zkMTgENxThJu6qOBX@9bXbhbcZj?~~s?HAXto5*Kj2{r1r3?GA?pjPxWR)~1T7+fFq z7@O54qhxZZ=bUd9;&K`~LIbD^cy-w>G#OQI7 zW4}Z%Cvtf;P`O>{N4Gxbt)gmb`O_vpsJ=b*_VLN>w1+{1BcWy*f72OHp2U8X6n8QI z*k;0|v+doDKn;h4pna7CN9^p_W(MA{!{AM`b0t~S4(aW$(C zqX?Gnic8XcD<==)@AO3Fj$RsC?>^!dvmOE4-mftg7c^Ip6ixh9(3D-(DstW?T1F0` zfBa%-V9N(%81Zlce+e&tAl&SL)F@f_~KDb-U@w3}^oDv*Cd}?D*BDD-} zx$I6za$^hBwwsl}79>V2>nj{(L7}}WQ{SNzfAQQ;>6DQe5^4H_S$Z_bt)xqG68YkO z5bvnX>&`-HWV^lq$j`E39wbh&H_1&G)})J7=ULlvuWP>B_E zDI23T=D#GwM8lW9PJP+gY)>MgC<`3#8={!qo;gzuyUV<^Jlb2#hpV4ejGe`T5^VropX8UUc--pI`#HX30jY|Yvxc+P9o%2@- z50^XoPg<^Bu)n6L`r~Ppz3**YQE8c(P}n^eQ-l7`_NKpR+n)zjuRV(~zkK0QwDs%q zs=?w1XFJ}FuJb2mAp)Hn7(~oftptNvvXz(#XyNxo1VzEQ=eKmjp{3;r6>stUTaV)S z?KDJPhe}l*b%;u*Jt`e}e|c0Mxjp8+X%zxi||D!VbzHp|)P{p&;gK3vT|K14piV;>Bn%DXyYqZiV?zCM!94|ek5+ip^ zHF$Efjg_cnOJ z-|cLMGpA#g1Q)UO*^z(_U5Gw|l&hBcbN_06EA(M2Gv)}#n<=Ptr~fq&8kNf( zdanS_EST~Cv@qBc^f>iOXX4nr&veuVQeN}3;w|NwTJCd=U)EyqvAgZPAnqWLl*dV* zqT=(qlgiU#V;mAXOys0`!~ffRSFGv=Aa-B4D(xOd;No zYMuO zeu~wfEe#K1l4a>NLB&$#cz^AN9X}m>p^W@1zBAbaof^H~IJ~r+m&1>@sy@cln24My zp3E9zovR8*TyjHJxkMB7l-jFRn9q%kn(tSMpS}4toi^rm!>KhO#A^>TN$X38(r%Rz zxBUuX5msT<_avh~f9K$xKQk7eB1c!U6+&YgS*tS7R?*t#;Vyy(1&}k{O+SpTm_ry( z-{K{+>O2eZAF*i06C6t?$fmeO(i@@usU6>gaVu{pj8I1~1fLzMRzQ&8>abVsP#b*dsSdmg&HhvJ-kwhh3JxWBo}q6z)Uc}MSFW*04!g6B z;XT0%gN0J)n8cZV&o0*D6XOD}%0s8s^POiyb#w{NscCy=vX$}0gQt_!KkFY$5erya z9$!vtfzK!;2jL%u9KU2)*rMLuZ-Jm}-Uc59Wljsfc5GAM7ZQt=kn*;#e-iZ&3V(vB zhlt5HuivOjbozH@NNCogq#x!_*Jx$~e6GiPh+p!j#y##;#Ty~~O4iiU!`VNBhOFSY zknP=;-$rg73Vk3q)Um(Y(oL0AIENUAX>}{9Igb0_7P@-V2AMQpyxTe_nZHsEb3 zi|vyWisvo_Jvac06(-Y#FC+6h?`+N|v$vWlYhTy80F{;{)4$2yGVfCqvcbqcICIWL zB}IoBv?)>%Y!N_stT!PiHom&?epoK(al=wQJno661Fuao8Y&l3Jd6i{_pgErnd&d>9eZg}WO z!&LoaUdG8*JBPF@-OUakaJq5`sdlx3WJm%w<4y*THU;SQajcudVVz1^Cx~14=~$qG zWz~BML3>}V-;}zk+u#3Fi_#d=16O2uTkpb}8Zsv&pHIM5eg5pfH9H^*8*d2kU=Wro zlp67~?<^Ndnd%4?`m1lwO4mG6v;&`fS;d_SYdd6ZsY{;SWw}q^r*+Sm9D{bs7=k3Y zpW34)j+!p5S)4~P9Zx)@W3c*upJWPVimXNyRfi|dr66?)^r1$8{E+A!kXmYz7bP5r z*vF@+2TtSxD0-?3kZGYPL6OjxGDKu`aVUaz>?N9 z^3#o{q)&OKoYMC3UPJ~@IeDQjPd;Ug`1Qx8%fe?FGoc{-&7JX-JoCKWEBHJIKMSPv z#k#nZ4o)`ePweM%mW(J!+dM~8*$x_N|5%uzAgbD2}-y=V0+n0aXQ;#84WqQ$%TzY7I`Ozto;F`|Q_Zbb zcsN9upCz;cp78B`!+AgK?m&Pal}%XHu+Ixex%JcT_T=Oo2pux1IDQ4H`ay3x*J=W7 z)S$`}j(k~oWp}>#aqV}MIkZ*Cyrs^g2=Q+r(I>65kb@e--n$@qVa4gUoaG(igVz(x z6jxzS##C)^O_YX}>ISeXB@$15|K)Hfc}TFL>0ItqmC`-MTJ~MfMw}rT-<@g!?wS0&nl9)ZOERgt*yD%4dv<-HIOO{W$;agDW=U z0Bo3Fz;W$W{Pu=#;BV{Zs?#BJDa!p6C3ErA%A zx{+(i@<4x_Rq@I!$yFib93+#LN;;OaHSGVr z&$pb8>S381qNXJQ2CM8%g>!D2b=X9%uy+4V^sVZ~!!#&OGElZ@83 zgq!QkV6;;4>>~pj$}6Mib5z#{_bX(tw1&ovv-2e{TbFcQg|<097{O+GkCh~&m6KyL}#cvEzW zDWCt`kG6&$?EJG~K2v~ll*lvp3ArJ^xntvtZ>w>>XmEV^9p(3k(*?bWQXk|+IWS-U zNXMjkU7n2lX(JuJPkBw-xL=NI8d1c-v4rOY)n@XIRcm>J<-K_Vuelp59~{#IsM$Jy z3n}|8y=c9+6-#ey7BBwT>eNgPfM!==Ul}QWy;99r{%pCdmoSM^YCn{GE#!^Lz_G7S zzuJU~EVqaFw?ER9i1oT*b|9rtamh@<3^m3wda-pcxKiP1ZI~sq>YNeTqLg5AI`}!* zOy1NADhw1{TAvcIBrT^;$eP%SK$*1mbNwY*W?q;iz{3UTDWnq2WRANX^tb+$w_gAi zUrc2Ao{0RjI`{RF!rYwBw&EBk*V(47$SsGY0Ad*DkBy!OKD38TIrn48?e9*`s>1prhY3Zg%XJ z?zn!ay&4HRCSAU@*?pu0vSK0Hwb8dm$VP+ z?)$z?#8hzda?HyoPSpLhjG$>uxnS}}x@v%R?yTp_dvAhMQxvrCTw`6=4jPuBj@8)Y z*3c)*(ita0=J$bogZfjIfd;h!C7Rt%Hzm6vG~UCi_yL;QU zh?{6{%H`qLz?w6Dwr7>+=fl9s5Rap2(ONMcXga5|vD(~hlwN8cCop(c`lZ_Z%O}XO zvaB%nxfKf;>lzc;YTxQ8(?dP*68Lm|36F|R1mpoCsaUlL3M6w8^p*v-Nah-c!BZu2{!lp%txw!%f?Z&j}UjtMz#g zSp};b*TUM~c~y)VTgrrN4%LCC7UtqgvT@9YX-DbA0Qm!OKU=q7e{P(lI+XNYdQAM4 z?&*Ddcz0Y`y~^KZ)6$f@gh7|pc*CW8;UInQiHi1WkMjM!<)|zAK^zfh&f5>p=O-h- zRy$6lh4Ow3M`1DS`t}dJ^*(Rn-{IE@9z(B>R$4XBOq;b;plKf}MG4WcZdhe0(LUg^ zMdi3)ue~6vx-3iUetPEMv}#|WQStUSaGR})#H9L_n|Dr?KgiSlDr;C2G~Yi|w(eI| z_7L&+%j*mD`6jqx)r2Q?2X}=qH8yr*G%&J!cP!`{RC|<}bgipqF2LnAYv$keKjP#T zx)XI@nkEolkYMlwJsjz)&o3u0i=4V#AuUNiM4r|`hecNA&38SuEP`-spa));bt!;BO?FpQx_B{W&Z zQ1JDZdMZO5N*Mp9sUQ&+=N>cjapg$&;c+d%lqht&=WC_GyE>&Id(Mc9=GRwYK-bW& zC#DzwR0wUQ9sJpCF7+|kJ7V!4bfxPxptk1AIFphBN?kOAR?z62oB;yf0CZwsIG-;a zUk2^dCP$@chkTWY5I44Zgv@x_MNXK1^R&GnDQdGnEKr*weA5l2REi`QR|en}kG%_g zGUr(zL&=8UV5k-Xf1U*~HhSBK5^jqr-Phf{%3TzJ2or-k$*FR$L&AV)_Wg@d^OyeM zT**3zO9Q;AWiK7;RbSdSM+-1=Y&|bX@&gSnVwTcoJK}T#zwmp%Sz_=hh1B|&V^UX2 z9j_BcRsF;-__Z%xc3zg4lz5b7eB{8`il;Xc-C?U&x93{B0Ll1`<+sNUH23O&>ETR-C5s)Tb1w=Y1L`otyL`qa{6_6G|5b3=n zBy?1of>NXel_IxvkPd+a=@5$2Tj)KZCJ;!z>v_)mp7V{7e`N3@d#|{o(^pqFGPs=YHKDw9tKj?z$2To@4X%Sy|Si$UgVMPMUUBp@I8w`8y$TS zL=t}dGFba7)+>Igpf9s(-c=dpslgM%nieD(Gg5`#j@N#$)uuYXA&3PE@1x33RW=1; zjyXSqmp$j^Kmh61H&`Rctz&~EMiP@e$WXQt>_Q^d{%5dbA1Bb>3#w8OJwG4()k7kz z`!N~eJ)VFTRjzJn@iwP~sS{$urvWahdyVq`1a;|Tqx<28J^PdN!v2%GFwK0|`ktj; z>=D0l@!YJq>|Kw{eJ72p^R$R0LDr{z)~&&v7|USa(KEr#oovAgo{06d``FzWn>}9v zklW8{n;@qQ4=TB=y^S*WWf_W@)<;UPOres$7Sggu-`&V)#xE8k_J86T>t4@3tM{qe zf%vfMb>;8c>k%aoBhKhQUkfFi2K}ONmnOqMf%OT+NQZ;TPtA6Gkzkgrg<J2**5dJLsL3LE8=`4bS_F+w}db#_bf_&ccoQh9}#^#rsydPEm z-mQFl=D}dE{*;DX+r@ug{6YrS%psuX<7#PNobe;{6&UVIyZHBtFc0n5$O`*+UL9nY zWMFQF5m8^#@P4D~?+ioFtac-`)Cp)1L4Rirs66`~GFfc#Vy{&FG{;)40Uvm6UD3W< zq%D$W-(_-+{%>_fIQH4v7|Q`q##Fyer{AA~zA5lI?u>2v=?5hV=fz5VXq+oUAI4F+ zei_8Pp(Z}ehn)&m2~JYu@s3AxHC8vLo8oaQ9#*Awkm3FM5ExDTeW}MvT2gEla1@O0~~(kXOy?)B%}%* zOC-T#-}sDgcz7+{HPf6#1v{h~{iV=cit9V+JeySF;RIHBa+#O&^azdxRst4Bna)LG=5&+Ekq=HVzTtTlWd zFX0^>JK6oxnuzR5#HV7O@?f>*gZ^I47+zeKz9^5tMm;RG+TOsd#>dI526_eTmen}n zrt|e>=lJ?G*829PdoCGo&)565uX0@d6AxI$s}xUQepiJq=5dLb^W+h-j2FWJ{@1OE znpnznFf~ynn6msSQ$+{4Y(-oG2JQoSMXRrYs48~xy3&OE3%{I$LzTVZ zXH)5a`5tYo0ZZ)wMa`Y*z=+h|5*iL~6N|Kx&A1K*itXR(O^0hk zS@$VsWY@tyZ4Fnp3c(%@_2oW}A%|X0(|i~xDbI+VkEniwiFa`G)&QjfU`#eS+fh{u zVkz6oqb~>$V9;m9^xQJsHD9OeH`eMi?OK`HHWd++t7O|A{?( zP_ulD`OwtP-X8s(F){|9W}-!}mKL(}$)+tnkQ3-hiu1lE#plghAJ7^SY4(kaz8z(A zvWtz7XUwD+=u*w|qxsO34HYyd822Ylo#?Ic?pki$liSQ`-0Re#gkT-ZlU~N9iLd!k zb)D5;CEOe-TY?VFj)K(^zd|{pMmeCnFk+%I^4-e>@Vq&(0Eqw6*VthEbu$?^P{iE5 zY+f%VJzl*{)o3HjM}Ae81GSBMOD??8?+Psy-HOoPs+B$NHCMJh3kF!V$aM5J^oqpJ zLK5DPF-O2P==y<@$Q#<@)&+{0Kfa2p>BuieqiF-=?h!PMlsx8LRD#qmQFY zmx<9)<{W|pj;0d>$(67-vhvuw0PoKFQ%Fr)s7U-3dsUNK@D_7pkxmD}G zD9_Wlna``HYHBcyyZ=npr(7Q+^xY)w0N3_7Ys^`r$4t}jJc>^CN~DdI(9-1keuQHa zSGh!KwKH9PvD4J^=#RRzxtQNtO=Ss>_VcA~0;k_MdW%(}V@@{n3NPowY8Uo|nIMi# zIPYx0kIn<~=)UH4nJ4FUv|p`9#laE3#6dpEHi)9vQVLo-Z&hBk8Hp=YCkDNti56Vc z7qGH-9RS-}(%5FO>}gug&r)?h!Hu$!d#{^^i!>v`dzQ(4^2MA#B?7OwF+OmIKyEd^&L~|)EvKe``2nqvSwIg&wq;GXfnHy<-8n`>ciF$ zI$s<+ZTISf^M1Qp5T6aU6=*cId$vic{J=B?RI4CXpQuKVsYTWJSs&&pJU$kc<~o7D zJYsqIA)9*7_RPHhB-8%zP=wKU@zR~D30z$?)$BAO#N2}{1?SD$XY$%Fyd-+;W(c!u zk!7rmr7t9vK6@ElA^sAt;qqci?&Zsg4>=I7VX30e>F~<|Ec}^&Z+m1;Cr4NQ-u9Z? zv75^$_9uBzr3VUHMb4OVw%Lgca6owMbc9sgM5=zezDX34UI($TU3C&G`lNiL%%d~0 zv|SJR-2*u|@CypTDDQ`Al!Xm2rZpZLm%$W5$^zVT$> zT|*g*+$L1s#?kWT%YTvi=lBg=Q}%1Q6ht;Jv})wElH4kalCQW{D}^5YHL@Q$FUS7V zHulOVLy>9qw@|z}$K{8x{8H*GNZ$jMXG%;hBKPK!G~2qQ4e_Ck8>^t=2Cvz4WIXG! znnb&2#>d~vy9IX2yDdDS#catwY4_ome+vEKX87O~F^p=D5lu$YA7R86kI8X2mb1dv zLtD7&M&@HZ@l*8YR>#wnXnk~V$|KREFV7=iV2^wgWm^zlYwb{hrLAh4R5gTODwy3l zx2_sF55@gr9GfRDO9a@04>sL7c0pjt9<$0*FFkRgcH3&->d`wxWU4MDLLY$0g*Edg zT8LL$5JTJ+40$`XdS=k0Kg}_MJxMm&fI1B`>b}Lk`0BZpaPiSGHd;>Q2jtMz{j1i` zt$Jlk_Vqvt!_o+osr$7kzmd`i-%4zJeVF3c&=U>JXE#|LL@GzpiW}aKgXvGPe92kJ zCuV+|zQh#9QQ*Z#N`P@xsR8O!dSifR>e=?kNm72N_jYSiusWg#g^p2jUK)M(K6fJp zaH?{swp9fIb9E~ya%Z4~pslRqH8MqPT1SbA*KPWZYXms;56;JP#lR7e?LY!ol4sbP z!PbB@f%4<{dYnJ~2=CJn>KLjszmGN4?D*{qd-36!y3U!2<4G9mS|I|-Z-w$+bG8x7 zn-$hb8f<)Uq2c3(KXk{R=y_F8+3KS8P7Cz?riE>)0&j)r^bA&aJ@Q*5xeiVJr-reV zj)&3TK`(=Acj_6`)lv^qBYr^`SYG;iLXurgGyr{Y!*S?)?ng%~WeTq5lH?6N@*Mm) za!VT+^tn3q^FBrJkHDXk-Wwy4FO(WSR4wC6b+=WAIc9ZJe|hMQE|1#bRF#7bQUUER zIWTxcw<3ICKtHmUT$}FS9}34V>xfkGZKQnN z@;lt{v~pvXed&HUmx8mWnuyWcG^5PPnbzKn6QTxnTGyvjc~#tmBVI>Ml$#bMjMz+b z{XPmx1m7`q+U-VWSiMicLvJ<=%&?OaUShKxLj6t1d8j1p4nO9B>WVQkdglz!f3upO zmu+b|IdzYXx(|X}(9k)!YP{BcRVFNjQTRcmu&=ed0Uq+6*~)%Bip$6E9AsGFez<+i zE$!|SgA{5Hbip<#u|LNNTo4j&Z;vnwq)ZX@-2-1x7iWU6Pg3I;Y#{t8LCiq~{P%k@ zxugU%g5C$b_7jd>sbiRvK(pQcJ3cJTbT)WPXV|Ofmi*!&iziC}?#63Q3T-vaIuh#v zS?`Yhb+2-lNhWeiT7HqoEbRsBm&W6z}5y9VIQk1T-Yc7WScM%j_ zU)*qQGWhRbA{I)kCC`faM;Wh=3>_HWd6MyBLcBPtg~A1`zdCugY?(3Si2v@9(I8BC zhO4-=0MiZ}BA|JS54R65)N`_o{7i=OYRu0UZx`HMB0MqI{jvaFGtVEo!oMTLSx$=F ztl~-ThJte~uINQAmZ!VL^U3W}rekD2<@IeqWXwHhPKbu?0p8TP^_lZ1_K98V(Jb)C z+YmDwBo=k{K)GS<7jaM-E$w1a-E?>})VE>zz5Hw3LLZ|R3@{>bL_Hyq;7wnwU|%?8 zE^ilg;TfLdbOZT}R73ve(FtW3QimsIc%I?vh>p!o(5`FbV2Ue=Z6fx~cmkP2w7O#OT3c@PC92{?^`p)W0+6Ym@$+($_zR6$Vp@ zrJ_x+S3&2Y5@c&Pi^e+7+UfT%+_|8E^Xa&!7`jgl2Sft=F-+&&1ie@ygRl7T4>TfX z`f#iCC=0Qood@N;e+zvEayen{%4~@em;7un;uJ(Xr2(*s35`LSq;R{1lKQ-;*19a~ z4QHogcItJX08P6Jd~?2R&q_{nUjM6*!1@j8LTf3N{*3Jr-COH?Pa=s#5?m~)9O4g~$ zi2?ng9ho^{ksXeMMwM(W5|{fq`5q9sf-!+ z4AWqw&4$=0uE#4wj>=pT?~J`oaERwSCl)6q*3}Bf$jg{*A5~ikhDgTNtg?HdLpI{3ywP)$!AaKLeCD(eJX#`HFxnkL-?oFg39u7W~ItRzNw zDeCvb+ADok)w-5ce+iE6o?IV}uk@x;YgT~4F!NcFf|pnR5J!7<4K+HUXSiU#DV>3LybvY zviTv&zd^pRDFt&?)48M(CoY>QbP%|tj(bU^&!@fa0BSyA6up?zpXLm%9j9`pLW*mb zx5(?T;P*0*9%|s16W8pJWA`6(vjPu&4My~__f5o3k({Q&=tp!SiOB6#r z5A&4Zxg_K>4i#sDF5Up}6w9FiSB6qg?8Gu?#2y&xlIYTpJUhVJb;|PpR4!9!cX!RU|NkeBBKfSO|7jfqlc~-*y!uA$M=2}%W zpjVDUlwh-b7L|0InWwRe{Q_-ms4(QZ^lIVONrG;&GIpq2y*baI+|!d8)S(4bmhluk8haz8L z^>kTMGD(9G-5EfQkl(P_OI#%dJ`ZSPoffpFQY1jEnaMKmo&k()&AK3#Eq(X`-#>p! zHut8$bN7_l2oWfqLn{(nd&}~{yON_!@*eaU?{UPSbJQ8Jwvfh3um^FaJV*SbrxCq+ zhur|Q%^D~_1NqsGT~s9Y3pQRA-Pgv7#Upk_R%y9L8P{)$?vIZ|%+DOgm$GiKl4Dgf z7h2P4*1Kuw_cOh@{}Ht9hsGpxe+E_xr#K#j6)URHq6!R1oUTlER7 ze(Al!MJgddv(K@-(h-r-5&|-WRBq-fC(kFuaRMq{X;F_1Ip5y28V7G_VI3%0-Q_lW zi2Xu{UKC>rujZRRa(I7dE0k&V&WLq4AJie+K~IAI1$~k!0V zRS40@@5?eix$68Ag$MP0?N-0=qpa`W3S@6 zKWx<=)Ri*Hn!az@c1^J|Nx3LhA)vs_zCTX5UzlAQ3B*|6#c;GYkk7&icu+ot zwR-VeuHkC%_;RXjM1I4TznnneLSH@Z=(GH4)6HY^pCvt$jR&0h4Vy)9l3kY2QcKXR zxnTgykP`_3O9f$=7mH(**+oVQ^E!p=PcTPW{?UR!ErcUteV>hPmf~mC>#i$=H1*ol z*#H0w0#A_DkrG85IA76<;P;=p$M^nV@M1&HnqF-oH*|&-LhDiCgZO*To}_WGcb~wZ zx(dNd>Q@~$?|>rp-J2r&9r%_ZUzf^WM>>FCKkSz5c>RcL;F z7O$@AfpfUHr(VB(t@5XzPRy%|oaZ0si}*Zz@NT@!l0p5~<8O5n(Dvw1HvxIuruW#C z`={2oi^JP+hw!$^S~!c#_nW0A?M>&0`B-MyxCcwLN%}r=30aw5WZ>bKfh{5uT`aIt zlis~IRLE_V_bKlkEhbNgBVYB+P5rRs2w#nM!zPJ8js04awmp5V`uLL7T*Y3hY?Ko@ zE%p6`_oYHi>*GWr7S^_XCJhwt_F#!!7R_?N^U2C3NH~k^fo+-vh}7xTOBq^Tx%ScQfp5{wUbM%n56>|ySCpR`% zc(bj(wMw{29t+wLnK{3-+4wxwkTj4*7FP_Rpdi6{3D+H`3Q+*IV^@Bc*eQ7i8Vz<* z^C#T2_`32S?`yDJq@P8SGg~@8IqeGe zFX|>a-o|DZ1+t0@CF|&B2Jd$KeLf@%pQx^~8LXOd&*WbcMLtRm#Vi7B;t-Lh8%)Hh z@F$0!yhbVOvjB>P|EQz^ipBH2SpWX};jg@H3PbP@5Xx+LXVmP;*X};4Zbm~h`!=tQK5BEGlAI6(<9;9jigsIdW9KnE&{K#I}rHDS0`QMIKCvVBQp#>wg~H1!8*TOJiQj4fs1eqpFO!9uQP9cA)zpc3a`^sGP!Fz zR5Jw%T$7vQB5DSB5ACln+;2L6YSpCHrt7p&hqA7W)sgS=Uu3dj15gTAYo@iqO&)`B7cT8Qew zJaFB>xUQq?&Yn{xgnC8xxM9hadG#fL!9mq=ijH9EHu?bmHL4GS8bG!c@6cQ6$3lt*R7Ibf<63ye4m6 z>gsw&wiuKej9nhqY_1xAPZBnP{H7?XGT!1>+eZE@g`L$2Jbl7JZEWQ|ik|rHXv_19 zk|Vf=IQ2g{&a+J}m|M}-qq4)Yd>0@+$+m2)nfNhBBiWfS5Jl<)XIMP!DxOX{8mX};GSxh!bf#e zncHFwC2x1q*B5rg#{3eXJFhdcN9UWF@u9Mr57fLs4GbS|^8lOewu%I$dT85AOmefA zr<$W0=f9p!5Asd+2w@g(23l=>6W#=m4$~}!B27q!y!2DR! zVIh|py|vTtgZoG6l`wK+_~tzf>xXLhNIyXR*iEWYpA1~hB^DbhbNJh!YR{;z?mj)p zYB3Pi`FS?Up{->XEV_lsmu;T-F9B(&BtWOy|tXqtH(N%-XM`v8>B`-%kyru;c7gW4fRP4eHWAO z6P^r=I1^R9OEUMn0b5pAaAy)FKCNlOtyDRcFK;$S4wY4a{?(yz^lsBADCPeo58$Co zSs}kHg||hNQd_SFSO-&d;oVw7&2wpNDyfJ+)5?G`$xs9vXM=!P@C8N~w@V1q;YFcl z;@vac$dq*1ydEeuTP08n!YeGx9B}DnFEi-LJEL?c2AM>Da$#HD0nF{!V1L`3(s8-6 zaNN#;{cU^2YqGV>zPdp6;>&PX%f>HRJKtv;)yQgMfXYjxuZ^jO+FhQ*Ga~Td%5oQN z=q_K@N43bRICBvD-=c6w%bw5aNtb@YZ^fi6xy;BonuaibI6{(0PNxfl0s;fhOfUC} zY)=bJ;1sdcvtu6{K~S9y?}d*(ODtiYLa>yxE$1eWcNtRJ<&>|{{d~@Qg%Xn*(xZ2b z4N5z5IR5-bxQp%{SG0-)b(*E{aP#t;ZXj~mr)?=q9B2~=a;=XJfI|mdwkV(0jklf-y`YpIS|L?Y(q1w$|Nu)A~-U++PlGbSNe&jON{bv|L z4tYSdK!md zL}S7ON>_~t$(4zDv*<;26tGnWF=5--$PHC6!f0!XUUAHy&AsJs1-%WL>?+IMi7XE#R8HuLdyy~bI1lnUA z?SDna&m{-2b3@J=))Loe*s#YFSPm?poW;A&&`>qUNf>TH`hdip31r)76AXHzXUgB^ zi)3!MzlX?etVYpNI>+UJlFzlkL{Kg32Fx*UECP1$-idLtD*iA4Br^s5(@Yy)NvgDv z)5x@HC$`F({%_zry}U23uolR9R^>-a3*n`=Q~P`XG>JQ$!WUCK~O3H+uD=qOSGJtbHVti%96Q%V`pqMY^%js{9x z*3WFZpQHQU!W*ai4oU|zj6*a><2)1R(^&O0Ke`omrH~49L-(hi@6a09rtKPRIx5Z8 z@lyFY-CvHtR|~d&fN{ln>dB!Q15&qW-O8g}MX|R6Ltyw!tXu>BJ)_cjfkz ze=qmOJ##-Ai$q=?H%`a#&m2{m-2tCs*#xJB!FayC%j-JbaVD+Kp5lrOA&}rl@H1$I6^JBbhr~ANxd`#!n6W8rB*e6#Ils1tNg-~vcbr}V$ zY4!^}x)9T1S+wnhsAbfg9kCY8@Vus7p{rxB)O{z}$}h0yV5aH3(n+f0C=Eu!?OSR0@0 zyIw)doh=~*6mU-|pYS)7}Xzo%IS^fvK;#~dF2+*;V3QyXY=+zz_pb+M_x@#2}f zj|$VX72xhGFSLa`iZ%FH)cq#nWJ1X($*E2YwFSQnqck-;2k<6k2IlA}8L&zqFzBZ6 zUVQhe(><+!-JHvS4U$vhOQc^Z9Y{ZxES@mffZPBRB&)aF%e;viOzI0=F0{1X2XIzZr8czEAr+^5IlHV{&296)QomVO@`vm3dq?2UV z`B;B2&T@N4wxN<4$~7usk!)~|J8r)u^o(67-(}gxja>Wt;$bL=&vGX5Xcv7^JjD<% zuoVNua!8h+df?jiAJNX{?Mj=W>F(TE$a3_R`H0^`(|asIcXyg(#U=c-$;|)-7A_^m zE08YIruG>kXwOrwq{|cvn(AR364jftmEJWsQkv+8AarEaF*(?^FyJlSyy=~ zvZDc?j>+RpQep#XC`*7lXv=%<6|}>JYN@C-_ts-0edaR!+@p*~=J zbQeEz;pQ~y^{_P}>LzVda7p2p(~OFU>xUnfb=P~W%dZ%vYhq-re8Y@nvaFhHBQ?CiAV%r*Ew3ZNs3PrKw>X6waRiQW@q@Xe1%8pJQO52 zm@da&=&f`_`&N_oG2m4RaFW))ga4Rwe%+*>fAh>e_TPvk zcJ;|TM1JR|c9gffSsdTZfC{sM*FT5UBQ251wS?be z;94n*7#BR@4(Mxl+^2Q4531&wjNXUvtm7+nhlm(~#HR3)vN?dho4`F5l~;S(^c|HguRac|{N9`9@M1-rClsZaq#pj^Y=224S-dxj z)DFyl4fQ&E;+RR%YQbr! zltbilr=SN7;769oy2rHn4?M0fx<7Xh#s#kyelf3DVZr+poG(;|_658&+wZ@_`&VSl zOHw}NXD;vNrCHOQPX%B&(*(b=T@0)bhcZqT8rC^3y{j5nKjAcY$K11BY!5s?hS=f< zGz!X@L^Vb;Gp-@m5HCuF%%@oeKtH*7U1&Dkbl7YZoVswag?*5QrhDI%=eR99rwcO* zqpIoM_no*pg7qI<{+IMGKT_@Y{{nrMqVV|S$86lA0%?Z8ts-&UJqPd8V~sKZd;B(~ zz)RC^Fdc2lW=TF|OPKPpGxMGazA~SC3T*3~HE=o{C3IE%C^4m!`*;b*ffrlO6x1Mo z_oyZh=?6S{g$US7et{#<7YJ9>A{tG=5QUIXDcy&{vZfZpa{6Hx%X+)OlCA36S;tG7 z(ZMDU*8NtO8HzgOb!YsN?|Kb7s!C}kr#z?7N*%E@mK_1H2Q z=2W=$v2Uity9|(($9V)f#^x?IOMV2s%7|nfb~D6^X*fXud0Jm|JE4PcxtQaBz>Fyp zm-C_vljw?KaGMj-iOG6N)gbHk?P-}eq-Po-bfPc^;doXe%Di6j5+C;kW$LKx3SG`s zGpA^oc#rj=Y~+0oQ?| zE}aV2t8n|>MIK6E1lIuFaa`N4bzI|f8!;s+&~|oj*x!VIt{w2SX}(g1R}zjUmL%hp(1rwXH~^qr#!96ucUKuA z#YZ$)eiVsh*77(vH-XTl6)N42GPI;_G)2|(`ES9ig14am&5>haszSO~K~nME(pmU_cLIyKKmP z(f!;?fI;)|ovw7hC>4xT(%&+)OM!~#V5^EF(4JIhkU{gd z{=IiV(Hv0vyHfbye?t;NiXSW8tM-&F6s+#u8}r6>L$>{W+TMB*_@75H4k^kJCD`>w zVl-pe!j9 zS8nY2?G4s$XzXEE6D;6mD#ST{GF#wfnP+uKf0u8wTK9G_&!%HCj#5O zMD4eva(dg!eZOUc>Ql+(Uk+&s?6M6+%IGPbAetCa7Jal#jB$i$X6zpLN3J)~xwPz@ z6J$lj4s@k66+eZvksBi^b}(;0VUvIQCeU&i{%6GPdJ=oeF$sIlKg(Yn<9g3A^Oh;! z*CIgroJ8LXhku>aWUVCusk#IZpm=Ocpj9k6X9$LpRunWtRRNH{b)pUpP zjj>B-vs6@lU#A;=5k0thnBy_Hj!tpuXap!0@di3ekV{!srrOY?=mqy?VcT2pF%va? zmuyvCO)O*%l4Nvk%~b_>b%UrK-*F`5(h!lFf>r4GSYhyRiPZQUJO@lzsn5`|N|(Pz zCqnG#NYoe5uTC`EqOJB?kGjEgfvtMb)LwE{PdKyt8pUs%v%OIg({F}sro-upjfXvg zmatX-G_Oj*34D&zBmG%*M~??FA|dbgzfLh{9p{wVVx)iYL_>G42X*l_5hh`U(M3=6 zG(SA;Pk^a^bCnmv5mwntkoUmzlo<(!&zfmx$-2nXNN&td+YCpA-+cPD%XL{14Icvt z{E@Qbz}kxPno(sfb;E2FQOj_KXJceQ8SMmKY$W9w;1Y#71S0U(s zY?8D>TOs4tt*6C`B*v*6#gWkeDA9j0(qwZTnWC(%OY(8fdmj0rgex}KH@;>S zUazR_ovU~vkb<%TmXqV)8pq6Os{SUa+>EQ_=aM=q%|j;J&!uzbl^sI&9bI&38}w^icUQJghWuuXSe@%VzI>VA>TNL+$uRkS=G32IRJG*1r2Vm-hIb<0YeX^=OvlfcqTQ@nIMybMLz2 z*WGz>@Do6fptZtUrOU_CS^+-~9?NLqoiA%f;k|~HY`A@?OPLjtsDruR@b?*q&G3E= zmy{bq(J;-iy3Z{8-jwllrJF5P+bm^2C6FqG2$!>5#mie|tXAR?j_$3mOzlFIcr0jS zuQ#9;ePO~FbIMI!rKINhu%Z_$PFm|W|&c+)N4wAezs?vkK zu>RdI@<(GaWY6w6zdYWKGOfP}7Xhb*PpJR;&1@fQ#h}i^f1iMwRR^xsC2*NsBOc%= zvRQ%5EkX$}9gFIh_sq&RnA}9BMRY{Y5_Vd;t3DLag!^{CiQTh{5H&*Hk>9l5FP-pRXm+4a$=5>N!{JM@78^&wkQ1JAjuQmZuBiGF! zCU4v#P{hkIWeL<)i3X?5X8{=cI>5JTG%uflipVz6GJPt>%uB74$SnQjYS)QpE8NID zTW!}c!XONo!YsX~w5zoWf}B4XgtPxpi7yiV?I--ats91?&j>L z-lMT+fHTkSZ}aN>@q9saWW(CkxQ|NClsT0a%_ z%e09S$^|fQlAH(;UyIuRQYxUa1~pnRod#&PHWAWv)y*DUIFc%>!9_phUbcV#-?)f! zguYPAk}>U2iuwW=>bwhcC8OC`Z-L8OYn;iPy1IXBQ5kMw zlF#Z4J^8{WA07@DO`W^NKfmpyHTvv0;|aGk8~8!nqU%^lcPk>39Aq4ZdSf3O?Hb-a zeY-mfR{X%v=^#+VR2H4~QEGv^Nwa}n>Zm*yh! z`ppGS{b_j9#QXfqNTOeQjtui%hy=C$aAGh3k>@M z`!V1gh3amGmP*C)nTL1u1~uM~J{MXUN=m45`>K&|0!ztv5}ghZsFoFXC)N=WQMRHc zqefreYZ_(Uyfr16Q1Q4hFNK`T5YzNl^&`0gRh7&imCb- z@OLHQb(fh}GNvAP=8S`eOU%WFb(^js-)NI0|Ie+?$b)JF&063zyq|6CGfoM290a9$%Io zw-XIdx(d#|*B=F}nX0rC8{qluBvzV`dg@IyZxi^vchoC$Wn@dXh@Lk0bQxs-rV?R^ z-7JQXHonP+z9j_+;exHtL_ zqDEbV3fM3HaE%`^d*dqN4%0cnMKn>f^loP+8*jgM5aBi45NxweT(nnRkyZTt@@lA6 z`SzvRw97*J!MHPiK{g8~_sFpyM>$g;H8FPNajIc5vB= z&h=rA^}$3rqQz|WTSb@ePwWi%wCxN1t4M#6l81$%FaYUK*>tqnQ!v6*`0 zS#j^|ONfi9_%{FNcw?X6cLp-cu#D;XG2VaHmP$2xS~E6Yu5@zWS8&$gbv3W3`2N-u z&8kgLz#m+Z951*cn38D9ZoX$Y{YLPPruOY3vZ0UZO9=x*zq`=%+=MOxiF4x?l}uM+ zLXS&bHyojqd{F~lyMK9JH4IjAnw)@h1CPVpYo(tX>#z;yd&RCSz@*Hb67nzRu9_`5 z|71>Eb=`TVzGgAJ@u6&bs%dMW9j6J{D-YZrrL5*h%}*aCt2ig*4^A=SFY-#5bIP*oFiYvQIo?Q_ik<{b=Xc4PcBJ1lz6#(0G6d-9VgDZzcL={jpVQRO!Fg?k4mhyCD#dJv35Y<|ofldiRC z>Vd^%lBt@6670x(XTWyD0z&MZFL%6tO!N%HoQ##os@ha*qG9noM&$4FNJ@H1vu zoz|s>hH>x6w&>9p~YbZ=?q?QdYnToyppP#N{*r4O& zQfr3Pn9xq{a@hl6cer;y$kdUtkbyY51jF8khaQ0 zNmG~;EX&@m(FU<&JF}g7-n|a-7mC)H3~o6KA9{nb_Y(=YJkgj1sOh}c3g8FRh^VxB zQm1D8?|j3@XDXFA9(K<+ym6lr;MdkxL-gK%)ypls-q~bok|!CI4P!kFnB`_Ar)zeG z(xdi$hBA#ssHY_OxXm|16P z`Y&)N#qObo)s$pS?>dbF?svV-65Uq~OZPvQl@*SSJ#H<)vevWRDZi74y8C^1)Womv zZ@fLkW1-BbcJqUf{_TIG?+QnZ3U;MUD&Owm%ZVuyc-(fms_*S7j3s2P(`kJ0R_!y{ zRbE*Zk$km#l}9ZTYvE5aZ&f}{K1#XN5gUk4Np88FE+)mUR+UG73e9*lip>m2t5-%-=InmUwlw(?_+UBu7h&m|nno{bB#qK;r1#mk zfb3nMFS0(__V_3|`#GK3?Rg%T+W;gPf0_w2?Y2DEJMKAR9L~mM`yX!|eydkHjco1@ z!~H40_JOC$&tPws^gLz;QF z*%zb0Hw{dRE*za@e|i&lZDU=f63XT zalcPbs`Ht=ii-c**;`+?I|t>p>s9@y(rIidyl1bREDdv8F03q@F3}DAV{%qO1oJWi zHZ>OOc)i5RFhMnswn0qrIDdz4FbR%+50m(!s&4e<=4GQVTAvS!0vLR*nB-IM*DTB9 zq+e8xI!}a&HI4y~Y-Lhhj(xb#cAr*S*a|B<`s_Vn(B7e~#E?TB{8HE6#+y2IHHO6- zXb8}oufMc?iWMWN!oz(aJLj}QO8{`}vU;Ad<>=cnKGEq;-dxK+DH7SbO49;ZC|Q%om}p1 zDC7zXbk*@2=WUChCaRZOb>Q=#g6w^2AfmQ=f)b>wLYKo60YP{JrKG!o^!?# zKm3<9%_?by&+*T%3tu0z0@qCI(%#E_^~~zqVRrgD$+55*J#r6(T1d?4Ix||0i~qbg znzayEZ?yrOq}C`90MZ;w1G0TT4BP6;!acTktZdaHHQrv)>Q5OOSlu?-lWA$ujqqyt z12E-di!I@Dql>s0m~wP@5O{WEsCS+T@|#?owg4fY6wZU^(?8A-uFdHj`X#TvibEjKPbneKHv_v;~}Fw^_j9vg4e-S$EdqBFrT@KSq_cJUoAE;} z0n^71BNj9yv_&9Ru|a?0!z}*XfAw4&2lc)na}MEca~7514j~V16^A&)3Hu)B9snA& ze!p2Eq{^i@+J3`Mft_>?6_5i0KFRS{LR~G|ia|TgL(VVQ(z`hSxiF4rMQIxDib;r8 z3>_==*Tk*SEhQ`IbXP!_a}&<^PcXao!yor{95M=HTp1mVxtZjf0~k=D$}JIE~U86O|wYVAQC4tWKX-c*viL9~@|?>3U~&IYQPlQ--y< z@uUnk0$FcJ{0=Now;GQVZF5%etrZbo3gt%bSJdaqLbHEH?4_wcg)BT@T3YSD75n1) z)Pei^H=qYl*A>D?0_3* zYjCm!%m|t!jhp*mEU`#uM9(nzXF_o=2>1U#SH8{u{cFI5zFQwv$fjO^^*CLIJxRdO zmHQr~u++8yjn@d@Eu~@BJx*2mH|C*=yFfL??_vQeor|~Syi)oW9svqQMNRG)XWQ>i z1EwGs*_4}-K<3F?2*x2uzMn6_^6ITq>K*uM;@niqV0eOYnB#1Ydu824nx7nZXOUmF zAaGYBPZmLg=nC#sp37oxcgt8t_Vn-su1+&;z{vk?E_gxwS3O7DBOL4UI_k&5e$BdAh+M7jX}PAnLDeK>}koCBD}8s)x$r> z$97KO_y3T5AEY-ub{-Wxm*7BsUC%`R52ZT~CqS;fHp~^kzZ{=lcw%&doTWQKx^`}w zjv-zO;p%3UCPGe9{RRxq0oDNtTuM;u$dOj(*BOH-4Fea^=`f!$YRdxSQLH{2MC8!Y zb4e{8Iif@>n8Ew-$m7v|uXBD4X$&QxeNujE7EdGwrSxWeoA!@m{)|n zPtk~@=TTD}r_?kT@~Tc4%Z|uv;>U&FjIZ?-%p*~D)SDf*8rPKnYL1W6<~>=GVgA4v zK8QXl?vszj8A5Y)Wxi-V^kn3A9eQi9945uCYy4 zR`ic_8_dHppp>0^geo={!!SJ0Wua2eiN+TTP?)q_c(5j`Kc+8!TrqRy1<-1x#H=T|XT}g9exzeU>V-?+o!8;XVpMa# zDo3^=RR@~g>x$#v`w5)}S*kWyj705hwbAmrYi49kTG-N_Ru5VYESB{-v);a^$D}WJ z);$frnK6_o zr6MW(*=pI*uG;(Y@$hhrSbp(Xn^XmZ;j;*@Ut+rIvwloaNRl*2E@`~CeSyAP|A{T( z54fSUITMEE!w%zT0E534n=*X5!X=YQ#&EYdUKO$aVi_bNKTM z&_*_geDrf!kpMbJ#JU-?T&biic5uGi@V_4}wWnfDmNDeZiP5T>Ck8Fpo@mI1gp^&R zBgnRdBPs+xmb`$rpjFaN?rmg)gm_C(97=BZS z8y+xHYIk(M`#Lf}Q+Ge7H0r|=W##8hDJqEFy_$zv#e6pupcJ!lYKnbkNi)B-RI|~q z*xa^Q^~6lD>3RCeHfB7rxe~VxR7wM)Gnb0Psm6)3!JK->;5(K;ZF(m(LUx_2iTNea7E7UwjLiboP~Ac%l_% zqQbsR$O5kdOv#sf7WsP0KI)ILO46zIHkxvMYTpV*^B$1nBpTn)WY^_;7w4&|{3r&gPVvqGN-a-N6A77-Sp%Z?B~`-Ups0fU7_-twPC&gY6Z4xxaL| zcTgOM0pa<5Am4XcWl+r}L<~R~w?EQPjb|=!C52~0OVTsuID*BSD<2T630$MqWMkmd&!gj0V%~}Pn@0QQk$nrl&!n3vXt?ZU4E4!b zQGj>(J=k{yWw0TFY<>iQvGbOLwSCn*I@`kgfDjP_#oy9iQZb{W{N^a(fSG8} zvPNi83-fF|DPGB$QfWgJp0{^`MAj6q{n{Z;u9$a9h15h z7uA84okfJ694CDE{PF*;OmkRzfBGzpErE!s|g>JR+pAATl>RU%WJyKla< zIeE;Zf*#GI+-MiBYqdNbl!IYh-)VinXpQuuepfA%TwkQM8K(Nny^gXXeQ{u_#8m8S z-N~#Rj4e-6Oq~_KX$JG3iaeVHC?CI4-7f)Y1+@?d#c-j-xJOV&hf6aKJ9mQl_l*U3 zGbxRt5npgG*RkDfYKl$8vI_NHrV%5+fWAI5DRa-kp6hG`|WD8f1Z#O{*D;mKfPdgO9-{s+lj!i=<*U%AW=Ev5^f4d6PFOZ6?hr-W$aYy`re_E~&w@0O%P&pAYLJe=+xa9Z?=1G+J+ ziJx9@TH+abpObBzl_E4}<{G0SGFz1QU3Dxx8U!`#P^<>!&RS+=;ciKnlJIbCh!SK} zg~tHT%Z!0Gi*9n$ysijyQGAn89*g1xyhM#KsoNlrk>*}dA^f5JUb3v&sXf24q%c?K z{aSrJrH}Ig8vbH8`Y9n-LeHuS*F6O8`|4 zYVElUGk2-W=6FT13*Os~X|o7?sKdBB?sTSNsku~J(hIqdd>31BGggey!E%vKlbyel zIl}f*3$bDj(9DRYw5D^SzEEOQo}arHX!FA35VY+%tx19&C0aD5b1{tk@j}Y^e7bMF z7n|WwB?iN%39%4M4?Qyzy@_xti~9D5UWsiV<8is#H@r3_E~eO*zzx%=f;Pf#h`^($ z(%>FXyHpfwG3$l zKQ^w{C^iTD^1;19P?i(fSM#?n{?VRvMR%EVAc)qFzq3L3mL3_;rDh&xR=CH-`i3n! zW>sgV4Qm!X3!D8aW>5m+9y;D5irCP(n2dYgC6(4_9K?jH`aScMIfD3naJkwvnF!Hi z95))!J>Zpdzhd0>1I$7Zfpt>o;BE7YimobJQ@+wr9C^a(@;mZE=@a9s0`*yPM=I-R zM9<$xIKS8sMY*{TA|?6JNw=a{M{2JU9|)dQD!)0?RJXprCwcT-r%=a9Oc`B&x7e$j zvqE$;Eg{LsGORMq#7`1$u0dD5uT@x((2q9CYz-WLqE^ELj|!CUHIk6B^sDPLMa}$f zH7nc62{)z5G(l?=5+EjaB^-G>L~aeaB1cJ19!2Zm5tFkhp$<#v*7P`&{5g0OMwoA@ zA$HbKz3jY}WRQ-!vQwd$PQ>Vvz+;iJ9;b!5Tai1LDEwPL`=dV|JtPG#TZM-OP?JRq z|KmmGPx~r3{w9@~paj5$IF2ll65g7Ml^SwdQmPV9mIar$aic#}^K`zN##HHySD~=n zoQoY071kYTcH5$X)B_W)m9cYmum$>AWls%^if@*-)|y2{s(RYZQQfqGtks~b{<_tS;d3vrC{>4Dz@nR+5Y9D%a~Re%L?9)de9sO=x*IsJLKvn)LU$ zu3deAr(;`GybZWp#~n~-tSu~m?O7r6Y?+_hYvJ@oLZ@+@aC!kQUt8q{RZOW+Rw-Nx zd{Zb-N*OpbC}Z4-$9NtGblKAs37w(vR)x&#HH!Aj5;~X^W|}l}!ONU{R;5hnerXD* z!*km>ayhDJ$2`IIJ+HlwNonSoUmB53(%?sqwiV_T@t95NCB443J&=`QWoqqGohz^{5C}zcd5<)<==DT z=%Aco;dW%c_4mZyB4)w^ASKonpJa`HxHR8``7XVLk4c5Nj9T2WwLV&K{z2)jx~MXlE%JjSKi@wiZSb`O zE{RP8>LqbQ%sRROiXId7$I(C%zu!>TuGvk4MImR<7{~nz zLyTi_Pei6$RqRO)G`Emoq*~wGJOQdQ!~1ebcpl7h)ZvPzA;|ujT}pSUwHzyyt6%H{ z!`!gnSyWO9O_Q|Q;Sm%iG4|t_!9(U-A!Pw&AR3~JYw@yi`|i#Ft~7nzWs zrQLlugumA9u#YBtidlhNInomXQ8=B87BO zs(>u~UF8O9e3>Pva1$qqs^Zu;cgI4<4uMe?J7%2B?+3M>H9Svlj9?iAsK z88Q@Oj85(!xSG(MtS;51>Jt#G{pgiuDzCRL*6ZqSgeK#itwyKeF@dkd6_if$l4Hya z6T)%x4P%`lxOhXd!(rG>J2Q9Pq^QtGkDusQVD-TA1ZmY#V7n8G_aO04NfMU;I=yyGQRr|S|l zq~^UgG3{gI!APro6Q0&+_uJ*S%1eE=%KD)l=!=mx%rG|z=0&?U>U_cWEFbUo7_=Xf zhvgiU5#+gNXR5=Xgaovt$0EsE$kmY^%fkC8fnnT zQbp+Hub{?l`CqDLi0n!(?NF&_0jBfajMlpQLP901W`xLT19%<8AvPvthOWAhzI~ita5L^PAXLUgf|r_~|BTTpJu zMNFXXo3TJT!=+XQ8+698ni4OmQh^w1FuEn(tHxLaVo`t@rRz>Js%{JKKITq}(c5iV zAaZ_F`#)_D3VT99?k&y=?;pt)=0~=KZcIj84^;(Le?B)ON>HHj|sySKw+H<~F* zul{);#}6l306!Vz+lIDxa8v_>BmE#P9K?wlyue438K~p8xCu2Wc#2*b!=kDr97>t5B~i$Z&YJnG5bU zrOeN=z10idjJk=Y7bJo+wDDeB`!N@o#ctFzp_;!rA@W8Df&*9sB;TgM3E>sDn1jQU zSO5W4g(=U#^w4}Xy%Gr%rBeBe)kCLwi(3pV`)QM1ra1M5pT!V6x#V=Un&rWDxfTg9 z`BYVu4E3E`$|z9B$ddMDr{%^&7DfR2G^k;8KN&US!IE-aZn!>= znS(OtpP&K}^yx5Kfy@->b0Y@fF*3}mOUR0}jJv;^F;i!qB|cf@#GeYH4bERiKazrNh!0#{^K64`K(Lr0gZY zYxx^5DePR?=uV6zugBzRhKMnG7(`p}%kvwVr@WO$ zc8A7ZE%M|p&YIU3s$`&X=>5}jH~jm0l^Q!SRz0Zwcf>kaSdO|Exg5yRTFqTm@RN-HMqF@=;Xs+_ z1|}!5wB9)Qq>2@%KN=+3uG&SQrA$W+;hhZx&CQOoBdXM_RCsjc9K+^cT&chGYLW2Q zlIoS%eCqBswaLQ*p8@w9FTNPKk4sR?Bc#(BsIqF|B8bJw7)skyM;Ke|y`_vC71aBU zj%zGvf2CIBTdTz@BRf|nHp`WG6B-*mkkFFL2{Rw_yZI{mb=vIiY05uV{=TEa3*-QJ z8VN%7{d+tR9qa2OjtaY0TUqIGv*WKNl#N z=RL8tpr}L|pMClK*&E8WGt!hiK$4i75F!9T~*3v$0C4XXd>_R_S>QxqL( zbb=YF*`Z~Y;+YABe7gi&lKsbqh=S&XooPzRzz;Lb^zX->z=xbBk1UeejT7MFwxJ6I z_epnJB{F8R<1i{xBNDziq`&$3luumox#oCHca}DL(-=h!agk28X{{OIL{=PUQPlPUXgf4RPg7ke<|O5l!&ACghtU*m)>%s zK}rOU!7_VD&IfJhnhweBZlEfuZ&U9!Wx{#Spp$&OHz8H~5or*@-H*@n6gTu9b*hsQ z^F_m1sQvG^#y2WzG$GAOjYwK6x@d53%%vv0BLARZQbYKQa~g?fxuOVTV3smEI??eL zG0vI4yS&4lRDv+Ao=}k_B*yw!&8lvwdnDP1t{#ZMOA3+Xd9(F8OCxV95FV|$WSRc=$yga!A zZVBP@2znfwRk?D`3GNZ+zAJtoi&RLWSY|%7NEF?uQB#(mBm~xUQuVrhbP{>GV-{cY=9Bq+ayJP79fQ%gF!MT2aU!NwA=x$GPUv<_DMma^9z`w|Ts z@2iK(g`S`!5~ot8RMe^dO-zzZ8F+H=S+SqA;sEM=s01LYTnzyCBa}f5Se@OO;HUdT zE@VQRPGuSz)`b=?U&+q@l-)1+{IAnFyFMN5sLVNB&9_|soG<67%dj1FqE(rxxA8k- z8!BwQd>GdTbB@ePi(k0nYcWJ&P2cmj3!^af$1mU8W@WJ5V2LY_2iN4)k+jvs^~^Y% zklZ5`Pv86PB0_E4y1$~e!JVNTk`WBkHjbf0&P(-EFQ&xEYiorAb_Ma|%Mks+fx!r# zh619ESrzM`{&VW=AyJBxDwzXDh3JiSRWq|9-k%FPN}Exif|o*}XaW}nfR}}!2xu&EdVK~&85bM5fVmF0>a0%!Et*#k z4{~d!1t53{_MGh=q zhn?&kELeM!*!AqzH#$!99nRbdzWRO8=g{;1lZWV{6mi>OQXKouFHs6tJT0)vlP(~E zJScxOvA-D367`y+aE|5UuRe}I&fg1?AI&Z#Y%T)`*Vyu*fnwj59~>=VaahA9;&LVB6zKR?D-K2U9dJSTIZ7QRBAOlWI}Dxz ztOsEF=S*#Y+QmKW;vP^UPZt1qIWI3qhwrCV6kT=|<*t4J$Fs8<6iEoa_5=jxzu#a5 z$sdiH$12|4srKZ)aXlf$=lgb5+>-6@)kgarv!(#&1}5OW2fve5Yr*9@TON9MW zF91!Tl{Nkspcj1;3XW{Hgh7h3ug>}(ZGMm%@@KD6kTYl?`}pKB1UhrM?D{Oqr$Fn? zCFY@s<%nn68Og zKENP--$P_>-({m9JS2-RQaHd_SPq|ekY{sUX{b9V6=%0u?)GN8oRa({Y*!-j6A<5R z=w8q80XT;%&lZ~BY8uKYyYn9C#SeY??R=l;sfbOx6DeEy)k)KE?4>N(YgyyAY!QIendisXN-ci2u~F@}yTnoj|R%QERtN?r&8)gZ+*@1;~@w0*`=B7yqx-I1YAcm&W~(KSXaa zfnk3NmWx*i;IsW2C4(SpW`9(m+K$p(yt*1D%Wtq4@BQNH|q^XXqXMgMy9LiZ`&lG_R}_d(`c@5P{OC>b`q1v<4f0}Q*h?JI*NJlK%yVmMVdyz}F3 zp4yu;*Vlh<1OM6ya2voWe0fm?-|>^#cR=__CRNn&&kXnUpFFapt~UFb?is4Zz;EeH z+ri=KcCOs`!j(vip)WWS5^xVES^G{Dv=-*ZlL4S?So(T{76v{<)>`$VN??k!xrP|i z_&wZ|-Zwk!Sq`q`URdwGNbUpVJ3cFK+LzGHtGh4O*F6UX+x}RfJQpJoUV0fo@>?zc zlfU9X^}XmUH&}HjR$q5XAng~*3azQ~o#(n>6XaA~8)Qj9|F~|t)7>1HXIK!GgXH|u zbGGdBb~NjFjQ&bCvClOC4;WIKBYQUr?`e(4MHVU1&A_j76DQk&8nG1R8j-sW@)KQO zBhPgYI7Z5iFbrE4{=(;ros*6q&dOK<^4VowD6&$To2PcKUCGpiyo>8z{2!te@jPTy zGUDBHLRr}q_ZeJOMEc#2BKk$if;rS-eAU}8MN{Exs#a@y3G=x20LDI1lT$*0tYAiN zgFcEwdXe6wdqPp~S=^uYt;3sTwJ~6?tzPI=ff`<|1izr8E^45!4Cg zmRN#|v?t7h9zC_HH%WEUephZ%kT%Gzp;k`KL)hwaRc`j1&C{q!(tvr~Gf|+T!BnwB z0#|8!?D($4FCgh7pg+{AxVpfcA>>b8ds@_X zxdI8*6{TN(w(P(*4G&PR=L}{tiq>Bp$p@_t9t_?fRbvSv3{wnt5O486yLGN`XD!G3 zi}C9czGbT8+Amm#0QG zegu&e_R%G&9cVdd-hrn7xVgUL1$M;#wH@iQ>q*x)MMd<^c%+xj5_xob_{ID-4Y>PV z(u+&artihY#5&pC7aus=6;5eE(AXjxkMSu!^0wYY849X>a)@T&y(2K;WL zD}{|&dq$$o^N1W0he4v;>(t%P&KnZOMikE};YmGh$cavWW7VRa@c1~&UVf4y4HCQl z>bQTf(e)AGg;Bk9`yd*O%W3qwIv#7vcB&yF%`fM6XD)7k+itsQ=JjV2gYV;iGOEf~ zXw};dTauiYOtqxZpNe(_X4;_ZO&Y^8)YRE`N*|GNLmiDmHVt(&`$%XPtTHqz1akGi zo|=4lm3mzh*o^wW$FuQ)nX%lincCBm)l9)P5NpTU0&jo{0g>pr$L-~B>;67Q_wXsd zHnK;$7?DW_*N{e@*)R$A22IGDt%`n$d)_Shn5Fo#&;GWU6;Vu7FGtVq8x$F5Iru-U z%^NDGd;ejl0s~;QwLRsfq3LTCxS}n5Bl>v6+(as0rzebw?>VJtjJYBZz`x0 z0aI74TZLXYNNZiFR!oN*5)E9>uB}syUvB(Jk#=EeKDP36b29mUYP9m}soh-9%Twbs zFPX{dfK0E~Zac;w_|^V=M4X0*Z~ci1Z`agb3W06~>=Jr^6n-z8goQukOI@P-@c_=> zoK{j_)cj~t;e~7Pj7fIFrcw(j{qlpB!p^=^c3F8fatJSBlO>SXO#kJM)Bm`qCAOwbM&pRaz^nhJ=*Oa~}WxZ}s16(#u+6>`H6OD-V1Q zQi!&BhK!a|Z9nW@rKcAZ`xUH|@5?26#$HbJeF_%-{LUY1-p5SwXZdX4)5B1n0$}2k znah`4@#YXfU_<-1#t;2SmuK=Pnu^>ke6Q<9rXF5IO^t>BAiZrrq={WsIJ=%rdl7Zk z_d!?S5P=s)W7M}>`+DVT)LS!9`5J7d$?NrzzQm$tur_oG$ z32B|wF#9Ga96n$Wy(I^}0@R8Fs0DJ-%H$pHtU;PdKCxTZDUQ-B(u}9g2)jqa51T)4 zlC_G)f-7xo!1i@J81qaVDhY)}g4PQ5N71v6ePDWby{OyB;8r+&_j^Fl<9uR~sde@X zX|$vV&jP59vOR;)fDDMCqn=~@|SNbcbenq1@ z&D9C7L{7l%3ipmc0zg3kmkNduNiM{++8zRcD)Gwi9=qp)uCBo|{EEefkht+e-3_A# zuHj~p?7pL{!d(5(9#pLXQlD9CydUb?T#u6l##PU zEk1^R<0uxNi+ck$8_Ua|RVPAX73WFVT7k{;6FF_rqMweh$eXNZdQqMK49J^5gJ}#>TsGB5;}|hv8(k-5e8m-xb$PC znEs4BaGSy)cJ9Mi31O~J>sN(`X_z_Xi}M7QhvoU-S9@RG%~a!m?)&DdN8uJfTg_Q- z5dc#UU-$=$#sUqGPnUpZ3KG^(&rX9p#HPy>6WimobaGkFU5Pi%slQK~0GSYoPIqYaTX?L2-p^)& zm5`_x+}_%qu}B7;wC%1FBt-Hl@3t61@OpO}j`>Gy2|(2L`b)+mc|@pjEj0bbOYof& z%4Tn0cUQF5@+74fC@yi5;C(DP=fZsglnh3U#W6asp~aX9|G8LfjfYL86TJgLE~hDD zPNhN7H8LP*NLC1&kk_t%VEB1o!cpFWO-QC=c)I0;&N(bvjN(dj&1zTm+xrTyNs_zq zgHcv@V_1uH%5z4(3NC*MLyK#%3#o=F%0>{)oS(lxvkecDS?~Vxd4<+2Wt#GHZ`rx< z-S49BBl`EoMT3p?u=A%SVl?|_-Cw4~aeYhaei%DklxT}mFB+3AYTg}Nk0jy%cfQ61 zwBufA^_@uuUp~D(^<%-P&6<2)blKeHWOE$+#L@g*Pd1E3lX}PpZ^o}!*@?k9lpEee zOx*cO!3Ptnt|WR=XFB967NthIP`;Kdi_G1!FnimMc#hFb@FW z)x$wgNH>>-AdxX6wPFj8YZNEMa<$L(L*ytOe$a~MjG^e75j5|O+b3$2_K*f!G`?Sc zaVxXJ?H25_DNO(ckdM8qzmGEr;<=N>7fr4Dc9}&XSB#=bS~gUXH87h<`D4%58;6_z zUWQ%Du|M6U1Je?Tssd#t{iImI?IKS)-q?*D6w_{RC~(Vcl}s0MgdvX&mMS@7cQ2Lp zl*nmqhr^G70?gH6wl#RbH-JqxO!F|DEcNwK)?=onM-I0`fC0!*x5AR!Q~NxZ-_JZb zJg2U{+6QWy?s8PNDeN$jT?~X=0_h6>f^F@QVdD=W{s_&ro^6eWKa`$dJehYwq(%aE z0mY47Y`v+qw4U6w%hWbGn+kF* zqnm7)AXb**tz5>05l;D!v{A&K?RQkgoYTq${9;1|pZW7JG2-7u=PX5=W~ABRtCe82 zUpZbDoHuMT)g!O`43i;fAq_fu`|-9{gI;b3Z70D;QN>G!+NcD2u3vTUe39A+Y>Qd~ zP$cdiP5Y_as>fGcW=tri`0B9(?fk5Xc_@QqQ>)3uxtQ8LyJH52_&h)J2QPPAWLS zjbgFkGVd)afQbDC_ihq?AERQ(LH_|5G_ym%%~x0eLZ*B7mFHaA-Z8e)4Lr3JJsSZD z5^k><0=206?(a)JDmV|lt(K%yPeY~?y#G*Y9LeuK={`~E%hA}sS)^S>x$4SIF0A#J zjUGRs&in}ujXrepr#e*5_S9(}r^8n;_LTq6?!H-Q#0BdP#Xiu*xQEkq$XZ*$%$}K{ z({5?pnr9Bf+eSZE%SH%C9wOB=C2q~yKdi?^+b>Pd#3nO+pV43-Vyb-Ss%DIP9bSj< znOb35z7SqE9Od@(jNcG-4r0u3$chf z%Ck;2%&N@rxmwg?<;N@zj^I>V+Uy}EXPalPolT2ow)tu<0rSmyy!d0=DNANj)@76~sd^lGd`J{qK7sHDMO7E4F+2-d||JqK|p><79NW1kJ z&(6bSTwIvNx93~BF?Y;OP*j_X9#SL2&(gQh zQ_RtU&|*!|!v%FpaBpGIWD1w+v77*A!eoYPmrmDD%vITyj4zSe9U8zSsYocXZ=$o7 zt8Z_v`{26H*QT;59$+R0JU%A8iAf-UqMv_?<%Cv#|IP$NLzgWczmQ)WwE@h~$UgM; zG{FI3V4&CIs(yI|?afj^v7qJqJWD;r$2Ad=K%wL7!O@qf1fn+_nZvfHiR_IOCvF-e ze>Zh*>qKrbz%p`qx`Y-eW62;-FDktHq*(#wk@vb{9%QYlRvc$u6AK|fQqIy$G=BDM ztL^{k_j&Eob9*GUq5=+$PbU@M66=@0 z7_9fEz0XG6MCN~8*uryp3smqe-`?`+-ZKy3i+bZ1LUt3}FUKf_`v2FY#?lsh|g98w-@Sm*k5B*e2((6rGSWY8((ONFHRC~TeiM(hNE6ZAigJ!H{< zf95$(PtdwP7=FT;Tb)6dyg7sX#$S234-eC@TDE$Doc>@4E#-G?5V)a-f~|<*peu@k zyOX2wMVQ*{%3ZtjfrB_rHub)MFF;}Rq-K9IbK=t@!{GG_FZ|tI+zsF{Vv4LWycP|wm#eOzgB zW?nBHx57w<{>FOqOsG0BPB_f$8fWan!{1r`CT`bNmLIO37!_!IH}{Sx%^N6cVmgSY zNXu#k7j^DfNb@yWe#oW)u#G`NUi%m1G(zhs#!4k%M8a}}Uz>NymH_FuR1V4pYIz@Y zUxNk*z`49jEHy26NhN#BxuL05o`rK0pr@fT%k3nh+Gp+A;Gww3hyO@AJ4J_AM@_ba8Y4>jLD-e$YJ zO9A2_S|C)ohjS5~N}}*}>5KR8#pJKiL%E8Vg7PFKcLmyaqur{>&MsDDy75#mQ@GFN zAKVNtcsF!OvE1$OXQ6ow_U6x4?MpC<YQpI=?p6UfF)Llf)Mmo4MjYl(Aj%!4)6(=g99a!GQ!r z+}r#Hzoc)squsuXbIaTW6B~0B$?ZlF^6*jAO4K_n-MNJyMm=`1E)_nKcZhmDFzk1f z%bmS$s54Dzu=f5pIO)qGy?aiSUpJTl2BcNAPKf^U#B~U?oCQW>DRwLJ@rWlIxCR)J z>nZ(Ac`07ohM`5$^vXPGOH%Sro-x2Kepe}Gr0;29g(4{)3>b_qekZG|O{CuLSaG9L$qA2u{?ld8?Lw9A$p&7&W_1c#PkOqaM#g zY_(aLQMJ7Ie;@p6E+_782Tol$boWAijM~Yk=En2i@|_R&`2y0--m_~CT`G@Y z4%Z}BmLKyiT<>gei@qz669r^T#Ptdl-NvW{H6|62+GVTtTBbWfMIsKBu&dRBcp? zSHZYU;ua0QlF}?ttt1DEl|n;uD;O$*Ic1+v*+lgS_-QolP|p?H>e)WHl95 z$G{vjV*cnTa>jOqw$ZH8SWlJeN2A!e>jlya5LQ%}cP5_*P2#lx^w^6iW9J#8XGTdB zpT3*Gg_tgXH!mfO90lrqZKV%Ot0zo;8dKzUh!vS&tLb3hqfBBH{>9vw7!fFAwKpTk z<`jlR>&(%3aZ8kCil!4P%nkW{Q>F$?C#MMRqp^S_zJU6ttepM?fismIk}(kDprEpnFiTJuv~BF`IIxFtQ>;WfXydHLEa`{3g^QS~0%=JE>`V|(0c zpVK@c#xD?#PIU;HZH#@W6vL{LK$ukq1lL{D44nawPXN3luvysCsK0OfQVOK)RqNC{ z&49rB6u<}ZD{>9u;$3$K;rv5jD)7}~)|IFTz5O6Y0Kf{%Bs zi>pI0MaXv(exFm*5C-AY7E*AxVh6^NaZB$UwP$a}!5+uBn48ZyKRe!_`Q{EQ9hWH` z*V3xz9U;uEv`UkFwAz?nV7ojJvV_OZp7$S%HE$t5Y=$hlN0CI7Z2wIH!X z$#nSXic?0T6A-L1r6F9j;I6sZ#yj)DLB#l)tU zpFXGJ>dUF3Bt8hns&*8{rm81S%IM9-!DWf*8gx{3()8I@^=kdst+bDo87^vT-gGUR ziau=(4sz$DSTcl48p&N`IOpicmwH(a@-}L8`hwE6WOWxO;M(8^t6I*rVg&w(^psY# zkCFM)(d7SggcWyxnKk59&f?A;Oy z!{C}N5?Dw@u~AvAAt)UzcM?@E1u1@4(}hPn9g^xR;^5wsQxcQngzhwp4TRZyyyfTR z)-O>@_^DPIrF019Gt8iI&%8DG%tk-2)k7#jVPtfsY1QcV{#-U`!w&bXcjm009H)+T zekb{^E(UF*`p@U3V9RSfBgg&_Z;7Y_L4rq$jLUO;TPsW5x#j?SwaZMBC-NjIW ztW!OH)x@#T3}ULOqxrW%+|%llvQk#Ob2Pc11vHQ|EiKlj%p~q(O*PINeV^Rvd7LHo zD_;$`{r!C4itB^f{D{jfxPCo{q`=YWbcm&!xmI?agQk*wXGQWrLmF$C)(T?`>2A%% z5wV7snTj(^%nrY8bBu}gUw2!_>$Wk4gd-U-%5NmIW%_itW}*bO>v-!5KlX!pHaGL` znSPd?8JxLW;C_Dq^f<&FC9z+3XlF)3ug;tCo@N6hFsC+Usq;h9bhKR6s9olPs|Q)P z;7jzgH%B<(9s-x8J`O+Eo7HFmTCtxH49E1Rn-=8WZ8JUjmH+i$duvA(dMYXF47o(d zikh6Ot#JQJ3-yV4-o=-uZpuio_Wp9!^`~@>n^QbA#hr|xxUMUUI-@52mT&O^Y91Cw zY<+`Dz?NRC;2T`ZxW%?M0{H$73%fm+bRoXXow*XetCvACS%mxDP)S#;MbelM9s;B- zl`UYX(X`q4o_)V+;65KFO*L&XeO(+SQ62!!yc2IC5{n%$nN3xBX-9VN>56X95!X%v zAHX!mWYvm_`W1b^d=&m44SJ29S+zVuP2r7_P_L^{Xqsix=#H*>l`@0lPM#9OZWILk zGpTG7aA}f!m=Cec5$+x!mj!2`0(TdQ<0=<#6tsStDK$rzN$U)I1pH)tGfyDVZ{zu+UT* z^sUTng&EU|(*`ttv__P*4AeYA-v_F_0?5~I2x^D1X7}yhC8tssfi8|VVqg3#z*T(! zMWSikBuY$}1HnZmKhp_vTdV7O)h25NFwWQO(0@RMkUP6x2xzT(sJPF~MS?I&Cxj=3 zVbGWS|8aE|eo=K@-&a8pM7q0k0Eh1G9zt3aX6SB_lQtX#@dj z-hp!0Y-42_U@b@IE&lp(qI_${0^bCUAnDbQfbcMf9alM0}IwPP@&j>WK0=qJjV zh7Y7SjiirIQ#ky=x;LTRfZ&~-BzEgq~rpD_UYWuS|_Zcu=dfv=9AS1y}CcOl>4IsfD1AdVO00Hhm6HLC|bn=g>?AJ z1F(>jB>rn)6fg^Obwo10ZM3JwcL5=$NS*50iKmS|pNn6XPIh%OKD{46Rt&2xV{@H? zFmxiWGbihQ5FVl&{kekz46@kLKudo-9T^1zsbVIpX?S9xO)h||{d=cuTE0^JGA3v)iv7;; zX-RPr=QEkAY$J15yGsJq17`MOB}Yx81?1lI9685eO7^WYT|{nVJt)tv(*5gYWsFvY z0Z&^J>x6l&F7F_lzN;ih`ZDV?$tUa;+?&YoQboVwt$vbcQY_2BRsa7!4HTnO|3!Wv zVo0&@i&g{K`%Q~ktbDBg7iCW;cBas*!Qn$6V?hoPfd8)_@^=uOt-=D+mQs}s;7)MH zxnm+W^0{<9`kE}5UnYZ!)H6zwK9yB7t4`{ZTC|NO-BvcH?@T#Hmc0?$2rv(gnQBv0 z;421NA{#y~?a@xy0bBZ)Ux5$QLK};~!%%9t2F`6M-W?>z6Q!*{hMEE}8whRvL)Nzx zy@jLP<6qVNvML&On~*I+wax;>u$zMpnAX?Y4z&Lb2b9!W-ybkM<6M2kkRfsfFnc(l z5|I4)%S4EMlcEQpP3LuFP^B;jEJjNl!4=_E}xMO3|czz zBN^p)XIp_=tCADgZ4@t8$qw~^S&?|3wV$o>)(U+_`vU$nx*pvdx4&{Zgn3$_EcTau z|1hX6Uwk9aS33LQJ#G3jr{Sh2Krj=Qbv@D72*}-VqtxPMS7`TPUINpNGc({qd;Hjkeoj ztGF_Z(aMBEYmo4Mtq*plD)wNu=^2F1r9;$2R#$dNhN_P8VlF62wpjT!H;byHRas?p zAu01VT=w}SUAD4+*hw0{p9*GKKpbK0s1VZtZLPgq7%=To0{{K#go}mBz@@!4|cW1sJ8`F95=L6|`0k$&1$u4NwoV2n-UeuBpKj;30xuOW* zfQC6w(DRvjo|cmnB+MH|r5@LU5v#zgS9>T~A#&-&Vd={Q5g}SBvrKdiVv)-CM`yf; zs*Q?dJWX|>u(0_TII0e}V(3y@EkA&~#(3DFDP%$D?Vr;T>|C|<=hCP^tFKF+^&Zf# zc#j7_{s~%CLdm%gaN{hN11dC_|Fm@`^+al)0v%4z57!mDN8Tf=d9KQ;&u1NB{gV$# zN4uqrwfOw76eCcBl&vab;F7S(^RX~3n!W^jMAEpWVsMkJa;&_q(J>$SM2elxu!=A= zqwkfL&TFncdTEq8s$g%X6lIZhqxh4MNeFW`x#qkv8|(l@u@NcHnkDhhLWvaaou)kd%cA*SE3uUMiJ-KIQ z#XXm~nyq^fOE)G(Pw~qA7|*7Y(1Etrga*!J4AD<2mn1X3fuxLGLX3S`Zq6L^cAN^_7*0I07n&b(?}KVxW-foK5JueSpolKRabUeMmWI+ zd{#~&ID@_VD;GW={yf858G8EiM0IwL3KCeXzNz&V^l$cyNl1@Z1!kAB?T(=9kt6uB zkF$j?y%Dy2j(b`Q}EaM71-dD3Cf%_-aCr>i#FLlWZ z`safMCTZwa;8$#rfrbPlQr_ZNKs~t?uv4801nsCGk0%B60SGKJNIB8;BD5&(cb)0i{;Gm$-9S#-D&uNn} zM1#*PW5_?&3O{%^;CD4i2lYRi#obT81g-Y7my-H{X<~AUw^;lx1{-4jDS-tYa;eMA zG&va0G{ID7KH2EZe021i`RM6j&}^GrI9cdq(jj60@a{##Job?;&6w7UF@R&EC?#qj zCMx$0H=XZ|YnqscZJL0ELYjcxi!@;`?825Lty}>&g|tVvk+_OhiG4Mti~j1Te~EX= zqjMuEpVsOtN*8?2W^eQsBEPR)$-bu8R{3?-t#4E{X7!m36ACH!C@9k=31Ln32UZHh znL`ya5ZXKy4qE*_$^@YkJ#F~Lle!@IKpiP^PfzQsv&Pq{BGKHZ-yGWv0k0v07p*c54juEk&T3jyhAcxlIM$$Da{EuP2@ zr*HFD7oM@d2%H}oYSM3ZH-}$8EgYs0{3?CZTI)ZYQTq`4V7ewO*jMP+a>Hx!9{a_5 zeyVim7H1Op0nmrGWxmPyPUOk=Ut`4Xpa zprGc_eJ?Wa64^F@qQ7U;xXYJ@45&(yQH}p4oD;YdcgoG2GbamnXJ0-=$-vd&HIJ(tmE&r z06-O8Uf5#z1hY0g`z_Do&fqf#<7NO>d&}NxbAu*SYeqt_b8W{)xh_jdvu?LB0k9gr zaRpI7kd{WjNL--}I3~QDbl-{DcFvRJWu6#~L%;A<@ev{V{Ij3%A1W}1f(<|gP5}^k zh}W5X_C_+6pNqKvkO%NFcYv8%PNKDcGo0;Pfd3Ta`_$V(31hkAKY*2tDK|J>l!ymw zV&Yo)Opvtv6!`m$>ktPIxJEm9@+Qa0e0kB}5yr%M&Jo+Wj zUBNb9GRj?As+lgrq}E9qF&TXQ66e=Hf!V6?&xe#czI0cmH|gr3YB59d5Vm>431c>`rFOnfUN`{JsX#5n2S>=9) z`5m^!>zlDMajFGulEpc@%P;0J4ySf!)(k<7aV|2LHPij>T8O2Xcvo&0pd%~W!v0&# zi5%wGFF-S(B%JUL>f-ZB?aj-jrVpCMuYN*`gU>mQw^DV)CMh-L^?@zGi5uKenV~b% z=nh~lC*cL6Vg|WFkxrPRHUm(R9?Af!9-VJ`NV}WuY3)jjozr*sUXrXCVC+uu6f^5= z4t;A1QW<^Jey`Cvhl4cghnNd{^sZ?OXO?av{5a6#9)3YG#0n9-vT&f>n8F~mr5>q4 z+xK{2C{;y0W~peqZy++sxy|BPxb>5@nA|QdSfp`sGx`jBe_{v02{YX-+{`>O1hERx z|9%J{XF0!7lVcMyNIcsftWn7O@e|nB_nNEXBEF1SR59vyhf{KdS8G3^9L~%|lB>k` zpTT}=ncHEJtWNV`9Z9RN0s}Z>(Jz6u+{WqyC(o_s#nY6{l@pZe9v@XsG1CEiem7J7 z$BeLsvkwFJJ9n>1D^dk8U8mlV94fqr=oZAqo_q;}y2V_~%;{^T(J+X;8nC?748%2J z1h8$oi67n|8HjWo6IB|83WuxL9eOIIn>{akngkaV8TR*sdWG6Jn}=!kH-*ZHjT?&w zm^JxMUam<4J(-|>^DR;W}Xt=63bU|7!2AaUrI zt|FHLVzuEYz7sa`Y%g~JkksCBXAmXzR_8xZQeRyb>V;rxoL_8VUc)!k>;K@rhWie! zlJ8gN^PKc|j~ppLkCSJ60YR5HLDNJmQo|!b#jg)hbs+dytD7`bJ`;sD4VNL)Gm5m3 z^BW-^g=z~1t3`swk;+e>ce<55?Ag9bABNK!$Et=C@~$9Q)W0HF-7CiXY5aVKm?m~~ z9m*ai{xmRdKL466uV>NL&2ND#7??06-r~e%A>GOgb%PVH%E7-Xd3li2 zayp9+{8)*!1wmv$MBVg2My@q;MRf)k2F*9WKsFXDM0o*Kxl z8k;9W2{59*xkQEygDQz@+>Z^*U7&l7p$QM-9pv~Q?@_P<4E*hfuE1) z<_3e`QUG|Y4;%zIb47BC)P1{bKl`+>f8qU&6x3wH0K_ayP1+jf+?K%H+9~17WVe|b z2#_-Qni@=x7#L+!>^(Zz0_l;g4&YL#N8!+F6hPbTSwNlj=IA|NFz7ycp;(rlDNq|x zSCp)5ZpSnIw$ezXpYTAAGS| zYF&zc{?FqQ3XYPZDa<<9V}rhdA9-qHfg%aCZ%iIvX@$ zu4-&#f6QlIs2lV&lSOG{6>e#7oE0lA?*SM!kEwW5(ICZyGWdLdKKV*|O9&X}M%9aME7a%IAd`NEc6y@ZmaxE$~ z^0nD5dP(**beO=ehg{a!WSSID*QuM(pD~&&D%Y%B9xYu-8ZBK-`KM?c&-us@GjOo` z{~)1f8p5@mK-D@W_q;Yd=Q*A^H7Vr;wYlf0`ENT)^^8$Qr3Tm?*~NFIj;v2720N2m6ch z?*8+Uy<+4Pi;+_NnJLBRpc?0&`#$5s!13MGvhaOZBx>M?9K}iMn;t> zkQK10cs`VmvTCwcY4p*A7bUR`Gd3cUO2`+@k2m?*&BBT-;~i=hBHbFn>DV&HR7^YO z>-0vGYH)ij0s3eZc@NOO_GFw)(!<>%lPvOJre>W#45Rl>QT!qnryuHbxa03>sQK?V zSM6SvGFU`jzG5(>kj4I`x0_~<`_J-_oFC0K@8i;73(2QiQ!`&8#w0BfNf|}go{*1d zg}bM)>N7AhC?Ik^O@y!0A_Q5QS=8tAUYoKk|T)2#>s8=q`zoaXBQqOBaw; ztgfoNBV5ZpwL}`K*#s2{h@`#SJ zX$;MhG?-@;KM^y~LYGBY*mzBgb8?a39SyKv&RS8>}^%_QXz8 zH5B!kwP@XE;_rH@i=~rfwmS+{*u?_m^MSl7*Cj?8lh`Wus?Xp;`V@)V`HU2*%2xSY zG86JDA_V&Kn8Xg>3U6I)7$yKi1l<=|XTFj;6>zKU@pnWljJ%mu429Z$>M=Wqi&bwQ zhUZiRiubq0GgKddx$!J2bC3xTsUQk@6MLKfE1vP|O-DKcH9Le9Lq8=NR%RE3o#aNRO3yRNYl z5@M~k#!-&o23`|I?g@>8x`po!hI*2u^G0PoKtJq9>l%_%5cTUaa;F#pcHkVeGbw~8 zg7k<6V-vMq;Kgolk(A8w2KP`4mtW+=mGjIfEtn~NBHMyOX^10$divCTy^Y{dm;S++G76at*l zt$keJbOhDMHB2MZU%_vIndWIkc~ehzrG<0mXLer|vjmcF3mYC7prfgOD4q2Bs`S1l zI2M8OMFCn@II+vH?O+oLkd4q|z+uv>+FXI=r$hS4Xn60*3v{+!Hubml?w6%r&M^15 z-7%FP2e9;5hNS9n(Eh{E)cfvHgE;BOeN8@%?ENni(@OssghMS@3+zGcFPicEdLrGF zfTCx#&7WXqw8o$QLzkIK(iaEWemq6{EB^CM4$35%g!gGol($qcdVZ-?hERf&8NUho z<=+_>Bk;{KbEoM<6Xt1>*6usD|&5AE%L z?BGV%5P5WVp=)ea8qWbSHtG?!exx{|SVlD_oqMR<(b$HRm7*S3R)u;(5LL`f z`Bm{g8!nU;0-7^3jKp=cX|+CCq?s;$-K2`tDC8aVHy7Z7QlMBDU7vT%#hYGFg|qcg ztX9!bRfQ0NeS3cn!uJIpqIjjH78v|i%^P0B)MIT(oqQT`Xm|fGWDF8uiK!QZviYN% zh|GZi&=yPX|7&uGcCS~ze(L&-T-5faqW1JLsa!dJzrM{N>D(&(xPZq1^wp>#QcIUk zWJ)$hyP}5AuO+lW|-r5ocxn+*HmIR+79&y0aCVreWD z8~kM3e8?~PEatrqNZY?*;5_MRKH+3UoCgPytplyc+i{hYCI$8rJdwR$37YXz&f@Jh z4tCA9fX;TZP_BW@irZu3IrmpvRWa5VPO+S;xIjjYI`grKR8F2ESl-dfJm!d@Wnnme zc7=egbn$~H)i$8rx#;M7LJ)9~gb$3sVV1nfeu=Wleoju{K5f7}Kz9jxvI}AQ*A_bCZzzyiP&i_Ntr&Q% z19Uu7m}rkR6|P+!j1@J@H#))$$mSvtv#5V>!%_N0z+{k#h(?xxaiAIuqgQ@q}6}yrHw^^PJ9LfWc0&9PD#Id8dz=hTE78+eu#c7d=AFCYME3>FPlShTL zG0k`k`KJPx1>K};_iB^N@l{3ZJCw2l8r1=^b4VXQV4YT?Q6M!vkWdiCiO!iA# z=;wfka&nf8xRPDqK4ud7R6|L)JPi=i*0KHo3ofDQkt0DeKKuOsWV2a4DF+i!T1cl% zP9en=>49^B`}HFWVkSV?I%7TNSdOS`@maT=T>NSfIAuZWx z8jp%(??h!iR^Oa9u>^kFncZM~8J0kqC1=^C{OFnRWMP)I;6sHN?J#zTUIFyn3w*R6u{BA~aNh?6^(cSV54G{7mBdgF=!iLosw|Z446H~#zgA=}$@vf5 zlV}0w9_|^?eFMNf)feMEJO4BSK5!qPNO)Ej7zd)s=jHQo67`}Q~!bh4xdwX?bb?XkyN#~U9im5YWf3GYP@l}EYO z5IAF!<=Q}LPcm?q7sfGJ1uGu~e^K5u@#Bp~MZeT9i4vNygG$=uj`x z8i@C^O>d~;y*6}Rgmfrri+1sd(Dl9>u9Gvcv>N}m*=De@ z!@Pi!_l_|2iqi>-FW?ddKhkvH&>fJt+(ld|zkNQ(eecnG`^gS~6Wv$G$H{MM3tZ_h zU9QSNOk;^OylaZ(V0a@^uE-5*mvkJT1xy|e0FCMkA4Do4T<{vyER_6H2K@67m+33k z97V7MKv_hcekF}j|1mBjWXxWN#rCKxTLcn4O$b(4MC(r~EKW(BjWw5JC~5*h zIVda3>kPTuon($bB)LC8HI)p74XIZG+@+Z(1F;`sqTe)=`pc8oyEhRB7ag>r9~E@k zjjbLgQaPvNFn@{*FrI0JahP@hB?aD`FE_LYI4&I#SBh7f)>^(mkd6Dx_)L3vXBH7r zs)m9MvGrewU=tYKbm~S(+A821)`yuY9nv)yu%{rC0WV{}DyP)`NjyY`5nGvF6)FG9 zekWIft?b4cMS53`BCwvh0_Is@?We|Mt9iR%enog?rYbY6rwYIcZ;YaJvphxJRJ1dT z7%%N{87qu9n|TXmiRa<(bairKBD+m+=nD3g>;!PFzQI@0_x#W3 zh5&&7X$9~*d$0!?yH&v|_gF5@7?;HKa zxUqp5F(x64%90UYZ)m5BGRZ7yVtg!8e*I>gH5PCbk66n(Fk&sk9dC|IDq6iE6t8zVk!p_pAI z@-Nq2ZQsBWY~C3$#Ki=J!81}I7{)M|hLlEk^APya%VodfY!1Rw*c~utW+gJCkryBl`A_v;bv#=-XH|9Q4#?rg3jY zF=+bJDoOC=73_pX#~tt+GR;3|{*t@H&VoNmCfyVqu!M&+{J4+qlrztOw@W;+@X+zk zxr3QxU?;umy^rx=3r+t!PJ1PAX?GeS$?%gWq`f}W;dyGn@p{@Aw2f-l@(ik_Etzft zt9n8wzia9Fz(c(yWo{H0-(WF0ObyTwh&}5yt6JoltYVwA?j{P8;cW0t2VRXJo+m&U zC8BDXo6_~@G6J|OfESh&ol%Lk%M3&kLPOBGHu@{M1g0>kWge-a5@&G*4eEhY4>#H= z&R1+v-kub|VXkE-%y3`chjx}901%^LzCW?Lu%3-8`e&PE+`1u_1h&@iSR(bX$6$ox9ew^1M@w2MFU&aiP>LgYp=qkamF3PQDGelsVpX!YQEL!he+9AMCu_(m~_d2gRhbquT|FFuabD zqE`tsG=E3)_a^bdB1oi`RZtHxewXOzUxlOoo+ zAxNgrWF#Jus<%FEh0%E;vvTaav=iZkO6%-)M$;h)`KJ^TQV#v8#;&nCHmvj9fA;G))z4Fgzcnp3QGwqjU=Y|VbGYvyg| zJQ@rtsOaBC>e97lk)_o|15fr@YNeUZ z%wdg2e*D%H>6&nOT1Lu*V|uxQlFOU49Xf_g+HqZ+(hSzg%A%r>JveNT0Kw+9NltLZ zDUuI9TU5uVRk1|5AHuzHJQ6b>FyMn!u|qFwb;%N*_{nR%$X3O+o+GQ*3D58hUEBv# z-I%ZNkK6?R>!}hA8!Um+r1xl0eo6U=v4dtK`hk_2X(GdV4X}vyYKI)N4z2QWYAp|Y z+2E2lZlj7AEn$bC|66ZJ=O{q|C0IvRg>yXMr~CF>sIfiGDJ9+Qk~ z3E24*HEDbef8Bpmb&lToU)<&=dK4_CexCKMb&|(V8?AUy4chQw zYl`GNmV}ATi9PbNA2=rg*zmzVQ}!AU(1D-Ng>ayN1niX5k*V+2{Z+D)K={)WU)Y5| z2yT*(RDU+{lDT;EN7AkBQi957P@Q9sF1{b!h8<;6mrHOOD{sc3I&Y|q8ROo^p&01f zA#{O~%1P3AD@K2fYyFHr3kL(!zR3eUmSHF!!(p-5U3H_Q}Q!R4rDnxVmarY9V|4e{qZ@ z>BL-;*V|WyWy^CF@!HDfV?Am!HvgX6fPH)WFKJoxFWN9PLQA;>xwW6~l>8%cWgSVJ z^PrSfV0!vzWFNS|4Olz^Ye4Zv1Y9jn{5S@dEk4Rp%Z~wJ(Z1@pG>?&$2U;~VJV)uq zp>(;*FH{4joNx;qGPPYjx{5kx_*G9TL~_Cte-}KPKK$?e3NX4v$TlI0Op8MUBN@-`kE|a}b~#J` z`uXW!&LNDv#Y*|-J-8M{a;*8k7?i`Ftd-t3SvZmwqHa|64A5Zo zoU%Uxlq5FF0YxRDDtY@|&Lwllwi93HrYddN9>`ANfH_KmbgT>y37-vGSHYJ-fXl33 z8y|Oje<@c^p~M85$a51|@8^GjGO!*H`)$F*8Fjw#nRjbP|L|uFXi$xmIVKmSjdnc! z6a23|!2FES-TM)O%#`oi{5jnlwP-UBO4ggZvn7+*i-Pkk3&!x0M`aY}mw$AEXzzP0!d3SEjpY)*O66zNSIS9j#^dy$TVp3!LLZrp$CB?TK=NKsmYXWIKYPeSj1Y-J9Pr6-RI{Zh)IHFuW8Y zVF|9g@^*&kpZ&vl96~IX!q#JYtA>M28I4ArP!+X!d#G)Xz)#CpJhvqmOJWfmiiwGD<&bSf%8452%tnNi5$EvCq1Wg&kD_(ioc<{uyCUo$;Ds@q1 z*%&J2+SJQei!4e8%oW}Go1rYs6tHlIgc|q^zRtppH(vCu~-O2sPHnp zN7bLYXq_Yl&8jL7P6!t>&r#OteC4q!3w)8<-fj3Zh+U8j!Ev&yKoQmeCNP>u>m<%) zTT<|SXSCFGu9|bsULY`>%$j3R)BSW+)2qro@fi*`J^yE{R2gm&RR!fQOa8?HC@UP= z#;>-N|5`#{@dMF9wZeoxeLj(nwc3a1=|8g=<*y7cH3idOq_(C^ASt&lC2$qr>$Z~a zY;Q8kj~I~kgTv4Ai+@fWowdhtW0L6H+d<~Z>|kM}9Dg0|b51Zvw{(AFOo9*`%k9KX zJ50S{g1rB!FzqJnA5-AYADp}(U1yec4V}GsUKpGHeNNy(Q#8kt#{D?jfpI9o7%^;} znW9*x+_&lK+@}-Eo+(BQZzhMVO{c(!M^x04M|-a#E<;_;N6qqJHXOzc71>M?h@NNnmlvE~myE!=8n4W@MS{&wP> zO|UlD*+ckuImw){zCj=aCQ-Y#dZb4=yH*csPThGeK)oz38d{lIq}%I1u-~|HOjOBK zuRn9+5G5;`|DPdzQcq4T^|k!SeKnk+CtJvaK;{34-`Kpc(?=@IvdGD%xHqje`rYbt2swEfdXs%? zA{?<7HG3NC&hwqYF!b}#wj~eO@sYbveWN6wULEIBpFNKr>g{;jiBf*1*|PE|8n<2~ z$Ny?xv)bptZR3sS@b4VKmaeiABWTw7g5$;q&49~wCY*rt^}R&Rrakh2mjOS%K2>yk zLKjMK%I|i3dy}XyepYv!Q@3>&9|GU#Cx~jOYXK^x89^VnBBB-9T49}F%{YTl6-|__ z*oRP4*FvM>+!8Mb`&U*ao7k)6n0=>5botH9y^M)iHdf3Ht`$N1__2B9J!>8+m0I-( z-7sY>W44q0C8tlP^;1lzRjKduPv~*qyIfjqjA@&LpN<)Pd14z1aGcU=(qt?w+iX(> z#=f!Q70ql@-?=9SEzP+)eS57I#PyVM>VmeoJhnz|UhJW;d6|`1Z_@p2xm2yS(l2KI zID-8!AHa6Ldd?>P(P?=-^}w^ag}HoXA{d?JZc+T;bxKpm)pkjyeBX}uH>yJ+VMLD8 z%C8`lM61Og*!h|a63xa3DLEx~^x|F}r+Zdz58=2x1AZeScE^c7bGZ-6B;DHPU0jmk z`~)TVF6MZfqN5c)t#)gUo8*0?u`9-blupZ=NuLlX?~HyH71kL{$tS#=6mVUrS7!T zxK)uuB$hhaCa@h&(pcqrsl0?03w&ynXz)om7B~ddURUPIuK=@jT(w)1YvMJo&I9o~ zLZN=Ow0%A2cfi{+`J*c_-o*c6{jFkrM(Q5S84crBMEr1;;dh{XTkrCwKw^R5ve=u! z=60s`0FRNk-^9xAUQd`^rKviOMka{9W@pGgBd9$IrBa^Jp%^o+q~g zEGJg0P%2?MoQBq!d|D?l5`&E8vg~fMnf_Ycw%tDoI$2O9V_xyDdE$=Mc1qqJyN|4+ zI@2Ky-Dyg#6=xO6QrrN?Zd5tq%Ay*?>G7#amt6DtJ9d1wTM^j?b4*9H)LFWB0nBmo zJUtqJ*dWI}TyhkxC$A%INsRoeKR{>^1=N3nRY(ffy+OO%8XmFG4hIKPI-}LjwLE~edQtnq64>|9A(Pu|r$!y%=(%9|^a*DdAwTt>E?lpHqFMOJ?{TVH{Zio9+2Znlx z*FPc-iAIOP83b73DQic6*VgY%Cx0F0axaPfhJMMmUDM(DUY3vN6?eJCk!*CkkIar9 z)?Ca@Gv~IhVqS27zp;KXd~W>Es^y$a?+@@=G7vW5u|Rw2`BVudIrpv1Hz0X&uXxmJ z9S2MA(&m*{jML?{OJfmmm7(6+DA!?d0L|qU((C`W(3sZn8dLa=<+NL>&ynVXbgQ-*KP}8e)EJAjQsO;3*+Lm!c@Up*RH~szLOjb34(P%QYtjB4B9dC}OUitC zIrKX$H+>p6-L-s@-fl7@4)s54n=Qo+yF&34zucD%roOqIcV6dFc^~$!V9?nc+O|x+Pa_I1PlDq6&NidD zGCD@Xe&ikoY<;Jz3)+j>hs@Ub$!kXKGGVJ|yd_m_w>ec{ZH0NbN+nOD78r(VuX<))dL$iHUd+{mZM&^(L!S4M+VE6pM^2~D z?htNG?oBD%W{NIN(Psh*`q9lRN0NcLH@7U)P<(|PCirJP zUB$4Ob|Vj$orCR3-kAMNXA>QI26)(uWyuTWQ5u&D_@93#HPpa&~m^u$lvy-z1y(S|NcvUo1a=9fH=6H3HjJNz66$(NV6ZaEAzA~z`P z=STAkmdBecVy~Znt2&A7@65g3ipQRlU%C3Kr>{q2fAs#+Pt#{CkMrX+nN?qn)b$Sr z8aB`PC5hF&r!l7}n*PV@3EM|feiOe!2?OUoTRfNoGk+I7Fl#%T`%?EcORGq52O_62E2d<-~}=lF`5fTs9qLy^JzJPUpNO4 zzIXB2&mr`IB>$*NZLFMVeyM<0M1r?196a}tL#2``+-AGF+{xYKi@xILFe8;n;t4Cf z>v8o^d0txvyj4%S9Vz?S_uDP_iDIYN=H*7sJwU?j(I0f;W-p|td!28%5aVd3-0$=C z$`Ck)*+00Q47V{WIHx&u%l(4tMczBcyY)x%?oG>+tski!zOFUX?Vhekrc^cRGo;`E zdha}qFwbcQ(cS*yfq?KuXOIcXvS~aC?#(G*P73U@+Yom~Ft8&6>3PRB`{1dPDywmo_+u1+cRi8RWy7KQdtKN!*>^TnT0(Og0 z7l7uQqdN5~XTtH_2ZIXiLrP^6C59l^>MvD*JD1^Pv29`AoTt(UC%&)Bh)J6sQpfo> zxIp;2=B>QG?r#ww7^WfPS``cCl}L*!_iW|4TV!EzYF#Cv~6RUyq@tDH}gw^)8g zc9nbT)zkHl1}aCBkQzDjb^>FjzcsSevES%`p0e`XO<3&aDwx^=#1`QL~7sfMl=KFKCR^~ zQN27Z?~j)F74*ey>h|b(a07($d_RL|Yu9S|D6Slh`S33F^bYPeQU$yxG+&bYI7p}b zC`jmCyM*^FY%zDEC)~W@)TRB)jwtD_nTb!?mPKNK)2VzN|wUNbQ;!PJrN|n2QY*$3}3U)hlhCs^;S}1gZ zg})H81kGy`vMqV*WFb*v7cSjHTI$O5Ea$c35i>&jO#Ga{1G{j2-9X0O%^SPa_1)-T z!cnWlM>9%b8$B6sSNIVy+&eAu9Al9^w{3@gcIm7yVblz_RmiW_kp!i)*Z&vFEVWwV z+8N)G&CvKy`t$Hro8EQqj$@+PIQ84NR}u2T+-6l~l3&mdmd}5M%g3LCtqrHsI_rD= zAjR1aH%3O^FjII_2OJ8IB;?wMnf$r_Iaqf7pqI#MA6KRcCm`Uf%=V1F{My$)Vja($ z$M;UInCMvWVf1(e!9sGMR7O=bW}YF4i!=?dBNqL{ejK+5(6W0Yt?=17pthS%3b*DJDgguX1HIasLb*+l9=RnS~$^*)N5 zF(1=hzG{w-ITlb6n)8c{+iU@nv#q6MM?PCFX?H>V#Vq!vC2n*Q9f6R#8c27p?Cs)+ zpQH0sFQ2o(1Lj&M@9Q;|T))fl59c=D1_Ln!m?tbib5((jm3)FvASlzQ6b@teu7QJ$ZKMn&vh7D}9WHVmYAC3<;&9hq^ri$`Y%Lq^pF(Q&M z9v9Pn!Q_o~)R>F;N!kY{nh+)iQPAytU$=}Cd9kZx+dIB#Y3vGVd7i-?6_bA+GliCt zlk8g2&t}dxRoFl>5#iz5=WND}7Ct?c@7Pr}gYF2%P_2e-(Tt2)a=W5|P8D7^RAySviy2QeaGq3I>2G6D(2VJ+I z-JSxcGJ_|V=Q_FUW)B~YbERN8pDBN$MyXbJ?wJ1U_)AqG1irtKIc?hf%X|}<#tQEM zGp0-dU=l;c;+4EyBz_R!f7f`CB^`(NiHXHQCTh#^C&(>kL)p?J*wq;A}?bWy50G={L&l+3Ln7&5yKw_$R(c9CtU9V_h*`1{ zW0-ebD4Gh_#*@u-wH9`udW5~& z6q?U88GJ>l3M^4_o%o%{qr;Fmr$mAnM-A;T;VA2fwsuq#wOQJ^r=zPNoa;uc>x-h) zZyrAk1Dno!NYxgO2olcUDCSPc+)k5qpfz9(o<({}dX}TEXR?u zeZ_bz#2QYkcmguX1dklzX11|~J%;K)LIcb??!&*^ectFfs!&pp^j+{g*R6I_*w5Rl zT6w>PvN|ZO_lc7cG2YF7U+xuoN4~L{ld%=AjrV9tlZlvmHKE;pW2!=5RXwh-?FBNU zkuFt<%C5IKb0O7-r`D9d$0>t9r#WnHvq~@>GLJToly2a!f)c4mHpR7hFW}TqCzZV+ z9#fAb9(KK-r}Arol1Oe8V8umv)6M-X!tvE?U)*0;dywUMJ@;*z3;9bK%zQfX=BFqN zP0O`w-E@DwpHJJ}b^3@}&iPdHi>KJ*-=JIBQSIaQt7)4|{yjgZC9i+%xLEga=eJQ* zmq|T=Z2AT4_)%H^jU`pl>J&J1nO|p6yFN#dh)0qY2rIMQ=Ia~Mp?81Eg2mN|^BG|!vB{j_!5EWV}1y6c}57mv?%`qgnJy?hHFSEH;O&sN1~{lS0bk&Q4_C3%YU zS1c;5(HL6<_4iYm3#r88$18r97t~wcA<9#XBvF;m#ANCgl1EZzu;=x zA`#B{39uXR7Uy$)cGe+3vspywpdXH+>sN1V_N)V=HuITYyk(c z5PNbL`X2%~SNrj4bV4#lix;xreI~;TT`Sf!o*y;M<>h5d>T~%Z7^EV;c>g83lWFtXQ ze&&uD^Xr{waaRoE7GRt{zp;Hs7N0|XU!&FOQGdUuB)rTa_B7r#r@4N;r&Yfs2$%JL zyzSMzNdm2`VaeVxefU4~i7ydLs`hdyW{DkrhgR_eWi~Y3F+Zddj$;1uU^MFheUpdJ zO(;@(&V;e4~!{Wr% z%Luu(arS3MX=i1Izmup){^mUnTaq9>-XHmZqoS+t-v!8;(Isen5dzpoG}`+b`;~0T zIWY!}*1&0hB35BI8E=u2N%nBKs+9tx=5c*W((5>pa!MU(%5mWS0@f8llK=Qz>#D@; z5@GoJNHq7>J!h|d(&s zPy}uRbAPAtK{;$Z59QBf{TX^L{&(%__j%zer&Z=wD^WKnXpqxXa~Y^@F8Rgn@5CM+ z`rGtr;^dqfF@}B`Fb~5NkQ?WfQ2u0eVFuV#n|h`$<(a+p zReJ)7ddZXjEhL%NGkUT4+*4k-i)m5IGKb^i|FG8o^jBK&&8MwWW8T|rEw2KODuBEf z`)MmN_X3ozH8#E=EnfT#KJQcF^7Q7y0KY|?W&jL@#BWmI*|S>+IJwwTlG^g??VIhk zpDF(7T$c<)eh~=m;WfF1Q$^HSW6tL|tQ-Os7$`pn?qlYWRg{lrBl%w@9ZrjMG&NU0 zWP6Y2Q@U&%Gb5b*-0I6@Z8!u5CNLAe#^V7(pCz;0{@gDEwC>4zk)3C-fSImlF+`su~1`Fm?(hxY1o0X#HrNUYjgI35xr(qBZK)hq*eQG^TjspJ!j*zW}{Pg#|z6Wr06YD1CcpjL!x<8dCd2J z;YPRJKmT3?$^1p9>2XkF1*M6n&zW`G@rG_yH+y&t5>;onE?ZxT>DM-VQqXii{yOvi zAvoxm?&sq+o`HUbEBmv|;lHZIRyW74gKB+|U!q+k#l?2E1%XQ6L_Iy$$626IB8FUA z(TyV1_0|EvCEF}D04!U*4GBQYMCKOyq_(f(RFELJT zKC-Bb4o%55GBB(9a9mtJU4+*zvn~!ibWiXA6V^2jn*1>4jO3-6m^X+y%X!ZHp{Ej= zsNnJXu(yWI`L=Ubu~AVVim+;xw%5?~>I-w*C9M79=J2XFCerg82=)AR+$B|00d8;o zyNnD!6+HI-mGsC#J)a*}3iDHbwhw0bcinra_HUh(2<;uzi5f2|+>u$hWKAKK?ejOn z<|SkE3uiZkOk=3A=pt*WksRLvs1;2-BXF%4oPN?|aNj7?j^mBFbH9)sofBj*MS9Sv z*Mz1iQzlo(c~7Ml5lUCKAM-Vhu>QydtikVqs`0`_DQa(Ky-PZb*vlPYUMp!qL>~#` z579vBy)`@!D-Vp8+=6+lr7NhSrWLz3&fc8vGw!dKw^rxu9um1xvuA40s3cx~=gv%m z=-93_%UQ7+uQ)vSZh_J7CnX)pH%wO23R-=`cCFrRi=KK%MoUbce8R;n1PzC>5lNhw zo}s@o%MgL@Gg*6cT}Zb%{QP-&6Q1C{AI8i+Jm&&rv@C8}oSVI-;)=Pvq+#|U zLwvn6JQwGe@c0e_;vHHS*Wfd%yho=EBh`GFbp8dN?pifh1 zMP6a~lTn3V6krsRy_qDwMZhvkN5poz&9S5<@s~eiBgDBRTF5U#uB(us$F?)Qj=(eL z^Jn;3f_I2T@~3yibfyM(7q)uunYs^>zac{2{IFuE;2FT`>)no3IZ(zQMcjs*3g@~*Q+*d+ME3co|gvdD34@D>vog!M-Z%z^6Ip3VsXum~~V7M~KKk8id->zrBj!W7}OfxOL^pdltE>6?St z#HCR4a^|^$D}_@ZjLk~dZ{wTIccI>2tBLKEq2@A(@djFy#B_Q$tAOIb#!5=MLk*n6 zeEsa{aqlyn;I6MM^X@rFPOv~Qpffgjy?qVhK~}iGZ)J+nAv&U{#jn`nX)$(+=!#Tu0}*ogWY0}L>((@zn)LutSKj8+ zqTCC0W~mMkRKjqx-#CAtm9+ss13~VEzg}YfXwWi~vCLQ?s>CnJY1zv0L@YVE5{Q<3 znQ(_)Oqva2jU%S^`9BB(`ou?hx-SZTKnbyElue^J`^E~7O)fl>hxZ-Ppra^`dE%O^ z#N-4Hf03!%-U0Ssl1%Osc=}7%kNAxI^z(M<<%NJ>l#7rZ*md)Te@)2T)Bh}G*$S-q zU@&Dvez>W)p|uuPXwysV2+F$bGv$|LGZ#7U<~Y2xER}FzU`7<{wihY4pfQG>AO(>b z8X*NQPy0&iN8p+{@`TbRu(`HZ_KbQ`VL43DS4v6B!?hCxJc$GY?MNEa|HeS{(U)X8 z-?ickxupWz&!Vm20xIJ8brX;bdRfa(EWaz8lb4Q--?OoadFe8J;b!{6)?lo} zX4kip;mVR+@d~9y@j_@b)A;MBv_}pUt<|t%Ys{lLYuDjFn$Aju88)>^uoGXORDac7 zL9m%2mPvM--%jga!b>x~bjf1*Zej0Yq8rb?wL&w#hLh||g38jyEhn1ove1LvK4s1! z;??8QPGgU%J(Ke_F|hRhU$Z?@hiV=94^63IZo$c^E*dM!uXs>e3tYvCRkhW|2%*;` zCu63IJdUZU?>w{vm3KLNx;SFap)6B)BQDh{Y*gpj?1|4bm@m{vz{faX5(!Z&o+p`` zNlxqb<4pV`q$;oj#%`^Zj0ZSJ%Nptn_w}lXBgFoTIAd9X97N4r=4}@St49wck z!%jR~7IqJyKS1mt0M(9%t-mnl!A8MB3e0VK$~^x1F>dnXD{Tv2*Y(g9sHFqL`r6br zt=y6+5YLpF>U+%-?l#)Q+`^Pck2{!}c{UaRXd1h7gLlzJD*cjTjJ-PU%DW;x>db}L z%Y6vh@v6m*)1)K}rE3n<%_N{?o$YmKNd#SXd1?%?wv?#uk-ZPTfce<7R|Ff?d8NK_ zu99zQhsZi`Nm-Q+QbVHIeGhfRc!;8R~Mi>e_Wot8}8R_a518f=?%Pf6~Nl#F&lNd{F>M`V$Z?vLz;<7 zbErCDwLW0N>ZBAy?bT4Ttrqcq1sZ}VL(yxx@=F)?ksULqg$7ts_- z_wfdK00`0Y=h}d&VMBLazmm7`Lk+!Xmaw~Y(NvMz!GI~R;i{}%+m$Ax)~FNwO@49@4RlvFw&(od zFQ^sM>&F>N%YF(OdR=D<+2f3*S(F}6b8PrM)dxdKXur%ZQfVLy=B!yLE!p0fWAbC~ zVQBddjao|X3{`mcUZTxy{Iftfic8w+tW)nXf29!4z_#mNYYM$~Iu=?uS9n)wy?S&s zw!(AH^rdQ1tNGe`-O5l2$1ieQP(fr~zvj8(_IF)`sH`%vgsA_KAgat$cmYMduYPY% zQgEwvRAd*^o^8qrDG2_LX?6oir_Z|z)s+U|0*yM<%*Rd8R05 zIW%=LA?(nAqj?6kh>|7&NM#&QZ8N2rnIiVn6cn2r3?%7|R>-K?jyT)|o+0~X#;Z`ln# zI^x*Wj(_JVi;OM5A-4DQXsh3QjD*bp>tx!Z)cLT^Q5Q<^cr`tB>DZq83Db}u9ljka zM%GO4F9XQQQFfG=@zBboP}vLQ5bMTDzz!letS9&`mq@?8SYs(DBq&QZW=d=|F|!yM z!A9_rqVP8?um7vS;imj=pk!%5uSQIX-r*xxut;uf=EQvA)|39Eef%hLH<#Pi7 zi?aoT*n^4jvLA&NKMs|gyKl^^q(Ihh%6eP8$pct|E`Jb;Rw3u7c}hg}gRMjvgJbxF z&$+bc(<6WB?#&{NgLWfrq0kmf|2V%QpGWR6drQhLBy<08MO{P;)1H?>5iOCs5P3mq z+#^NLfxfG8txl;CiC_2LyUgZ~r!q?u0Y<=5*d^w_Gab8RP}Fi6N7Ql_o@HRdZw*>6 z_K7pXO2CnT>G6Rs@2!(ipW}E-wlQ{C&$+FITnbG1iNHdT2o~;?vXJ4;qKXRC zIR{uHa^?EU53)sJqm|;CTso?E{Rirk6AXHLJfA(kH%OFQ)Z>18eI#5DAuBCE5rgSo z+o^;=htp*b6=W&{)N+II zJZB^8WvU@|KA0uD;51j*AE3I5uJw#&-_N7Iv117%W^H4rUgA!~9*fwrawG8gZs#gF zsObqAB{7i3y_UT0tx2+ft)MoMUo-!vBx5S;Opkqqu_WN0-s5`Za`i?>g8^lsDz0Z} z7|<&J`w4QgUB!N2Ae|vMKtW(sZ)HAPc?GN`M+WoTEVLhL-T*^y!NlX;GM^{9Fs#OJ{7g?d*~YC*Cq2*L03p)Q62N(RaN{!LXeWBo2&xZ+^{ z9h}k9w3)Au+f0>vXl=^QNIt&kl=`CF%kW!R%_aIw7jn-hW_AJ@n2U3?5jB^jM~(B8 zEl*}ZmRGcbtQbY%hh1`R>7}`%?TdOIZ3%bKtqRjMf;zt#X4-#OZJ^kahdIvAP+yrt zqVtNd-%_icKbawW*SY-J_ObX8`v}L7hdhx{C-*q?L!$rWlOndjbk5YKTJHK83L=$f z>wvdGgM4j*BYa9I>YC zXj?GGM)k%0k$bDnvC-Rx!JsdJ-Ee)qQkAH`ymf4g$inC+TkhI7kEx;HB9U+UIEJ6% zRiqVt`td^6=@gYLJM*(%UZFXxgP|YFr?fI3J=^vI?hp=+Tv)cGC`Vy|@_(PoM$&@B zBuDo7I}Q=pj&F`Aj`OA=)6~{-W0il*w;|L1gWq_z@{Z*nL=>+{e#r~jcwC)LSS1xBg+;*|ErTiS-cM68 z<36O%rjhNegO{cSlVjg0K%#S5L>Y_MgpLe9~YZu=Ce>@f%C zNSelP1Xr3Flf~O?e~ktC2e-?tU*v!`?w99ANTeRI;S%i2?lZ0e3gh%fEHebSDIGHKP_ zm-}#EUo0d0EFUa5;W52V^zIl^)F|psz9Z_gsPf${Y~xvrr)8F>qq*Ba7Oo{vUn)C4 zdiD0+-B&sM&LgX#pwg*_aWBmmL?)%3n`0(8PC_*H>(PmR2ae%Yid$S$&+-!EIRZ}> z^PE;8BZE%a-L$rvTfpAtqty!5b}=*&noViT38S~CA^n&u=6^cq%XEBneBnst<~L{> z2#n@~7iqX43$mKhlRn`dFNrL0G5p%29HD{BWJ-@)q}@Ya7YhY`()2sOwLp_5j2(IwZJMBfzA2s2e%QJvZwcVkTD(vFop@iuIJJd)TK%G~IZ|a1Lw3+n0C5$O2z= zgi9!?+Vm_@XXv-|R$i~ZBp0SokU4XXg$avyE^ant3zyI`R7nxUOgK|AJ%#sRn*icq zsHk47$B$L`&edrNZ^`%jPt%BgiOd0lg}WVAMG5DQvf}5G zJ$@NXxS^X|v0+BUGOzmA%fOpkZBMpCpbx*Yubyr_(GYt@w{Vo!ky-$@7a{uX=(dfJ z8-bHzBNRGMXA>)o-KMv_ms;UReN-AED9eu(eemNfp#rC0A>wwSBgY_kh5 zYIt#ESha#*`o!6K9oPzCk@#S{RbaM;tUK6Ms?LXbO$g<9%o#I5chN{eNuM186f@34 zBN}SOAx|-RhMf~gs^y`qtntSTgE>j1{wnJ$kZsEczlgT{#hZfqrijiHtUPMu*L&la z;6)n=MMP&wGRMx?8lYKN@bjUL>N89~DMpe)wRe32TeHdIi4KaL{)=v64f9aOr-h+gGi*B4-!7iOzi5 z?4rFG3fr~h7+BMR*W^ceXp_+wp{&cm20nV~ ziqwyvl56j*4smBhk6Up0sZgA*YnOhTa!b@y6nj;a*nEL)dYvjYryi<&tCtlK7JD6Z zG?D0>G3q+yKxPWbdKLub;|?}<`Bot!JErbwx9k^p0rKF#SI@8?67VUF=xF1POaK#3 z4%UbsFXEm6_NP7pu$m^l+-o?Q_U3n@Dy0^1qh4E$O%Po8yCZrF_c8j)irHv`ny~3r z=d}M7kRN=iH+}5wwRib4I)Uu=LH(V@t-I$EdZH)CG;`gGQTF(vTFIk>t{1Wlff!3{IB?99T#Eu*8ymgwk#XnPt-5Z(x zEsM=Cas9r$cVyMSHLVOMhUggQatHS!UF|+oOU@;ea6klHx)F^V_;DLI;Qe0-cArn% zP%f77jJxu$P0Q0Eda$(nJqPFk4Hq}{{&!kRltT>2`(jLd&Qne^u3G~pUowu9vq@F$ zy&N^}Yrutbs_N%lkoy}YjuOSs@nV5sn%?^O7Zc2(BVZ9bUQk_+vI)Wo>$rrS>)u@! z^_<$fw2_4SH|HhdmJfYJ-lJ(tzm3B%3mqgnz$W_<0rn7?MwcIVg8t68aSg&CdIm<%>pInRpbnWhqlVqF;LJJachQ5wyiQBzA(A55JPnKNj@T_Btx^3UUHWeikf<^ZfHtWZm(zMHZD!dYqPx)$MM7Zw@C%OrlTc9C8ck zkW>I`_8Tox*_l3AQ8jlPulYR99EF8gu)kqg$NHPTatdwQ-zk9dAjiC&=6Lrb;#Xx- z`g#M%a&?Q#kB?qr2`kw3@HZ31*`d+2$>6Ub?8_ylwhb}g3VK~?VON=yD63QOxw5gq z=wO9SJkqma?pd?uu9ysIO8pF>t&MVks&)(U53e>XVdEk(y1B3&Cs*X#7Dz*}LbCh? z34$LA$*rgAc~G8K^sKXIQ_*b zwrd+N`Z5X{pAG!#OaH|0RAhi#f0VQL(DZR%d^_xDuq*)2Rp&yzmNY=+L9zGY+H56& zUz~?Y-_gpguN}uBqyi|sxw9Y+DHtoFQ%Vgw_h+3uD#~&m-~CY~k3T|FTr3gy27A%C zrwMOeP4KrBcYK{g>aHePGT;JV&aTugABKzpgl$Jdfe^sLeG;arhNm$hps806uf_Ef zq9z@(8pv1A8Eok6{+Sb{5lb9WkL=2EqAd8Wjx$s7RH;dkGttX$K@4$|Q^;E>rP#28 zzO@I+uNi3Xu=qmdBv@t6#yT*-zdBe!g*2}I=D#kJPnicA!pQF10-PH2gwR6wdMvGu zaB^Y7ifG(da0sRvatH0BmsRv|>0P(S&U`jDoaWYPF}E!K>2a*SjeM^ujr2%Wt?^Lx z;mMTXX297mghTGUup!!KDz|Ml!@dJ{V2uwd(c%~n>Ic7q&=H$;=0qGFf*6}xU05mN zK8b+O2?WzRj>?<3UTx`Zj`T;3GGBX5sq%`&;Wnf%>+gWmS*G+|6~Bya;n@yU=a=zD zaw7SK#~@PxknN~kR=q>--8ST)EWJGUEq};wku$O+ggiXZzxPkci>Yu~MOho&vEPHI znw17rjx?`MRxQY2b=-}a?iAz1QvBsPs|@ni$J$;3hf?}4CP;H=B>=BHLlLHRAs5u# zkGL)7LK>-e1d@H=d&RX$-@SCu=W)o<#H9iMnjVwrIck<%qexxuO5(byok-h6B+NO! zCagAIwastU`c0YeRfa@a#;E?@UQbs~^5rZWB*66HAsQKgMXBTXLrP7QGczTU8XW(r z``^z{&93Z|#be`CBo`z}veYS7$Icrz_BXM9nWd@;_2laCP0Eeo(?#XY@UV|+ObmQ1 zUi9;GP908HgcUSUqcmUn&4S;yi08{!(zw!9KHaV$Rm;E~np4SmpPjesFQBhJ_r)Y@ zJNG3ZkybBw)#pY0gb(NIfnVY?u4O}8Pjb^v#z38qTFoXvu+eY7K%J~-O<;8W#r-xU z_rh&{oJ~_@6>aRyTEtH<#F_4&v0q{V%AGD&?QV5lln#)oH^w{aHDN8OPF3ggg_ME$E9%ueQ)Gj=?FzZM7EQDsVL-3`2>})0am~BH}a>lmYx7C&IG-)7Lb)hd*vBI(Bo()&d@>(l+DF0xD&PI8=;#C%OScMkT z%z7+AlqnvxMegCsNxoXyQ=0eyOQR>}>(C!Zj3h^~Q&BDkg9FLOQOiXV5iHVr_YiU| z^$sUSo)1C;=B&d(NsKO(9}IlH9-MptAzl7xfKhs97@O~65!>eTl9k&vp*nSHiuds> z4Zb1Yt(27m?m&uiepGFyqIq5qBp_`mdN`6z6YcL#3_PuTkKqG0fM03(qekXb^QKQK zG4^DqnVRVMnhGnPk<-kO@)mRTj8!28-{keJFxZbQI1E%NECiQP#IxrA- zl*>BU&DZbki8FB|gDeLUiP%-ze65{3ZCZZ((^;@q>%xo8_9L~m-?0W_3UtSKn0g7- z`oAF+u-q(d;k%3aiNIsGtlxQxq4?xMGLUd=tJoSo?lzB56f!_qyPr?d9y+KjH^Yxd( z@2dWDlb8vyDmpYHWkKry0_|Dw7JXj6D3uI$b+D57S2sCdIxpTqqckU3KR`da%zmt= zwKoZSj&s%q;^z$$oUi`2g_xwJ>?CmgsU4BBc|QWOKW+Aw+LVwQ=4#ax)vGXSrny-J zz#rx0SEDFlAI1KF&Z~iL{j-liR$uWI^CL-q^OdOlrn$zK4r0$6J~l?O_^cf2 zv-W@KhAQKrBh&GdC-snqJhE=Sgvx+PN2^a5Y=gR$fh{(pSh4L2W0;kh4ouojO?;W3 ze~2Dr1;ZT1o4%MtQWS`&3i(?(Xa#RqBw%d10cEzf{s_QtByM1xAi4VldTLl$(shzhjL-Z!s%2MZzu$Hk+N`e zPLq5C-rd-R%@`(#;y1_arFffXR*;1#6Xucoj&3%a{!z=tEvCJ0f{g>!7`dY>cOf`i zL*0wt#o@v;R`fK5eFLZy7WNI9slY*Fzzqkwy}GTSO5 zc9!Eiw^hkXxv_HGtn_mZ0^^8Yh5HX-NZTRi(9mo)GbCOUm+!$A9c zB=g<(Q=YKMlqE>EVMUS`?gnnOmnjX4nO(x^>7g}2ERidO^CUu7G3f)2fr42JNxZ`!Oyv<3loh-2<^#b~@7cnvNlGU5B>`SgXCDn_2 z2@?}_PLWG7*!D`pJeY5;{?0xE1iYV05DziLYoPEgxZjz$HMtu;E!R3&QYDdq6`#`` zKwPhBp#c6M%{+Q?7nQV&)5}uUsYi10iJSDK0xxo=dE!`{LM6*oN$R#&nzD;)Cz9^t zb43V1wgAreQD@i1Oy2!ZbeJJ&Cae5T)djxGFBB})+b6Blsy8RFe6Q<0NbjRXr%t)< z7pI8yp~wk0rx2iO37+rH6FE!GHjHKc)lL}KTO?J-5l6E~KX!8cvAdn6MWaFyxY;vO znJc0YIaRBSUKZa(eWE}AN_g|Rq~Hjavn!{x$w{!|g)cGH^NQ?`+H60W2;*Sy=Pk$j z`ChlYffaRCHPK|^YGRA#{+)N`_qxZ7WoXTn9EH208WEqy$nxD+gTnJkEQjh`#x|Xa zY=T!?Vq+V}@sf6z_Z(Qus_MJi-JMYJnx$`As2k2&a}~zernEJ2kkLy!#o_nbxB;W* zsuB?w=6 zzf!%oXkA)Y)m}A)lWcim(g~C`;6O_kpnhxv{jWHc5=wRL*juBTG=1V5M^hV;ex;Br@%HePEcL~9%J7O2@&Zi7ve;g1xY0eU;^vf6MVRtK zIJ@O;s^Nm`*i)}ef^}B!BI+%!XV6Iz1$4V&aef-94fUSydDZ)I)mIaScXp#lVO#I* zwLQ%|#jw*4dQ}O_j({-1V(zX$hz2C|k{a=pcTN6Zw-;NIlfd0jmUNw;gSA(YP5khN zYEE5ta_m|f9)70D2fHz)B7@1x5#Du%qNJ}k=1M}=^{shU)t#MqImWQnfmcq6nOMj% zXu>Qf;ls6tj7pVb76!Yc0_OOx=!0!f;Kpi>F<+UiQ|PhqOuyL3pWdpq!LY_K8jtqz z@?Day1opyNX1&}W(tGd2v^#0TOT)%vPDgVHnjZ8x`aA8hY2g4$r{G;!NK#%KM*r!S z9!fvrVyq>WDX_Ff-yb|nSIl}GX_AsMKKmXk0WD^4DfbB9YCpliJOokB>)Z_N*S9rB zEdJ}aD(r#I()m!l{2yX^=EMfAIj00ek1iYH)4#ej`(#o#F6BM<=Oxsi0c z2Cm9w6C?W8#?^#r(&!jODBse{iZaw8Qoshrz<$HRa+z4`7cKP!j>ZL3{H{G%_?%Dk zJ4Gak`?)j(2#E#R@&4&-_EmQQ-TX+?l6|!9>53_?kjY^XRZ_g?Sma$8pyq)~A{&DF zV$4iWkgVbyqU=mpI=(z%K==wh7;$ zEnG1QsJf-XtiKSr*$9i9HKuZX{5scrOK9QcSC_$8G~3VEhoxLLEHJWI;1_+3>+CPy zudRXd<8}=tZz^mb{N~|1;`Nn8>ObD|_^$M`7^r-n@*>5?Dh^3);dmcEOKG}1`tfSr z$PeG343!@(+rzFV~9CK|NfP$35EDZobc^$6sd~;61B2D7D4J@6LTthI(XY?bt9OoTDqk>2UWwYF@)P1KFM!a zkfvt)pKs{~CQVoiP)ToeagPpa5?sz`Z7!!L>b$Mj-SfhQ<(y7+fD75RP}n)b=%SVs zHFzE%df4fwK&-Fn65&n3VGo+rs*U!j*u-Q2)Hx7yfry(Fu(RaK8zA~~hXjn|ykPqR zENoP0K8su*;^$YTYa@)@@pm6yeUU5a_SNQPiPwBx7pCeqOJk5` zB(~X*_2^xfVPapS1r|Ev$=0)v^7vy!veeCFL)NDN@&@GrI162{PqOifK2#_4@bPl7 zL?2&*JZQ|df7qu^!zjElxGjs7uq)oiE%MOb6D@i4&ctLEOR)S6UkRc*V z9QYDCKVa`d1so(d z5fD|GWFDA?#mg)}`BgO}troWMk1tb$5!~F*WuVBhL3IX({PAFL9{rThV`#XdX}@5S@gX-8THu2n$>OfE*RD z(AdztsDH|g#CNHc{dN8Rr1L*`phxOBe2IoW0?$_MyezY0L^D$Hyx&hx7Ax@*B~jfS zGusY6)X%Jp@}7z*gu5W&)2=Bc`6JC{{sTF7foc4Pj3gKw5hJo|>}9&({y)k(Q`m*- z3aMX7`g+U{S)A2qbl)18bi?kDiB8(Mr!bnM;u;{~?Re(TePe|PR3L2ETaRQM2%>Cb*EvO#M-C%ZaOx5fTRf> zYWG;jm{<2d-j5K9*TOJPI>=hne7Z2cFkkx_{9l0R#Tactj;=fdrJhfm&!SX%08mN7 zkCMt(zsp}L3`6fTF0dhirI$iu%8Gb zSy1TwT{>*>tqs-Q$M^bar9`~*?KXEX)Tl3Uldb~*2{9CTFhg)D#nq8ld?MnP=M?Z? za>Pf5A?F5cl&fQrIPlWijRC0)&5-%q&rHU28QLJ#}?|sk@DYE^8h3h%j5pg}=^B z7{vXlIdMUb+=ZSX#CQMuA;|);boGZtDhk@149FvkS84&MGu#u@P0WEpZ(qh;LrMbX6O6==1N}sCE2kPMCZiSacD# zCr+~0s;C6+S96iQZ-nP7vv-nuDb4az0%1;}I{I2KCK@moSj&C>pRZKSwI^Vs>!f6( zn>fN5FaWF0@*?y!fB*Y~*5Jc=gZ+!x)*JcYx`@SQ=R)~BF9hGe->VJg`dGzA50+Gy z?73OQoXRnvurn{7At%xE7s^4w8?CtBlv=Wq7uF`dX@8R3Z7pl&A0}C*%kVvAuJzp^ zvP6f{5s~X*B@Z%LWy)5W!+DYY3zapgQgN$lviP4W6BjgH=Z61bZX`a7-{NAsD0_zZ zGou$;tm`UR$jPmrxNRqqm9OTlD2(J${`29p%O?N%9~6{m1Y(&81ki77eZe0IS4i2; zDlo~yviJCj@72ZYXF(PTCz0-L4H4Qrl35;}ts@nXq}c`no0tDwA^YfAulE(QO?9E< zc>&W!#S^ciIXFfPyioD})7VeJX`7Onr!1I%-GQ=f&t{mn!f9SvJ`vuMxq$u$NLr{% zhQAP1)_z$K)jdb)y`{8om8WtNsin#imi^+pw-k05@0ysVHFzDgW@n+)ki9QW;plgR zG{dRK9`P@H^|Sr30twWUYXwR@uUc!9>1QXjKY^Rf&oo)_m|AFC<6gsE*Uti(HdQ`c zT*S32r4uzPms{`UEp9;-h=EEtNa+FO+Yc)40 zo+rs`&)aLL=31|zs=Iey&Lj7zfuIcAnfz-hW$1UW^UfJ|za$bi#y;;h>>_5#Exp#l z-s}%fSBkD%afcbQVF?f%c+32j`!D}A!z+3LUzv@Kv!nU-T)}m+0%_O+jfF5siu2yj z(Ec}>e~TeODnj$gK0@7A-S8nGkPq)GP7Kg)yc@{F2sNe0D-T4|krx5{|W?zX?CJ zE|!)kRtGs+R1vZ1fF-vmrUKTzO)@SU=`SEX#0TRGtE_leC0PC{d_Nk0owKn84+5u7 zIYzvbwCJ+87*l{Ruy|z^NrVskILDaFopVe3E!5R;-8pCcN0-V`in;c8gQpC(Y(|&b z3p8&y>n9?cl}9}#j^ep%KgIipMn1CK)l5>C?R)#(J9J9ZkNe$}VN}xsrP8>?;ifmH zt5#+eFlSNM&9F5d6dB1o9^*gZI%MFHb}NwN|JG?Tiu;W~a_!{+BF%Q=ISufbJMGe22 zuv8x()8B_wGOIAe7@>VNdegjuZ~klK>}0V5us$-Psn^m@BA|Q}Ss{CK)P}amrlSD@ z-QL?n-Wy8IULwqui*x}(6QqdDareAQEP&_Y_;7ZVutZn+lvRXEY{3>K15yQ)N9=cd~p3|K&D8f<}p`!ddI%-9*iuzXJMj^0fyg(sF?_oFNrBu zrk0B%Z^`j(%2Ag+Ch%?VT?-D7s>gp)Rb}ZDakf=4UZ1|~YwncgvP3mjY`f&zYu;wD zEbX{3{d!K|1;IZ$rYtcM0nHK^c>_P~2Xrr$sL^y;?OHZ##??RciWa`A_jj2Z)M@F8 zn$zf+)hM~o1blFv9XE7v+UiQFZfw%M&q}eCY*=iPJFRkZS)LQ?K+;AReAD(hP^IYS z^DhCc*rlYmdj3Ao{h82uGNO#4uEm}MHaE&i)DcE~EY%G%5vcjf2@{8@t@mxW~&-GU&O{tCi}O74g{#-9~! ztDEMd^(urH^`ZKF(VbZ3{|nun^72Ucx4IM*maXdYeEGjlTm37IPKu?c6_jR+zeWq& zm>gbr!KW3xv<+f`YZMH~T&OyUEQm1JrDx6QB717>6)Mt)BwEdRw)*g~TJtyv)dWI< zc&CQMQh~yh!!}v+0US+1`wDIKdJF#vdQlorJ_^q@fS!$@V$Tk`SWaiZwY#^Cdjz+Y zkzQ330sYGh&2k^Vq}j(2_ROPj-DW*;JHpvwH|_QL}Af4DWY1sihOCFGN7FI4x02I z>+oB=1#=#Q$ZCU={K+VW8nV+DHZ4orYq6_lJRl8))%;b(;+Ik;P9`Ri?|iK&Lvp$n z%?mRhtxCpM04SN9?hNq;SEsC*U0oh^C((+u4E7H#T(%;8KWY_j0=v(Vo12=XJz^0V z$`bvabQn|DsOsBNDid&Qi#F~(a^h6`1>W<}zXA(!Ev^Ib5p&9rSJsF1<*v(6JoT;k zXd;9~g9^GmDOOtd+)r9-fPc)!nQ5}FdkvDc*Hx2>ECxru7w;_f+33GQam)4#M}k6z&tq8W023JK<(-aYuHgVJy5s}Ph&-Ml!|E6 zVUW1uIKK1?O=ejnh2)h9xvC{%$YRa^9>d7K)p}V-H(oOlec47N8TLBvzgxpkd9-yyqx)E zahPa4g-MBaQHMRe_|wIrCmYzuxYj9E{Ii#>r6MU6Zr+ z!sHSLcCm1FWjPzsSb}-fRo4_)(Ja+0l$f#B+~ntCM4Fd%uW;LzzFd44_<-|&`3BJ< zS-b*;Uiqw1^SJ8~K>CM@|K*n7-q!LH&Bk(mCSLW$@*1ProoN}sv~dF^QM>i)!6b_l zomiR4ua?)QoZ|zKLdJ&iJy60UQl_kE8O63|3)j= zr=kxTrhRu=)}<_pG02J6?b8OwN^J-=83FbRM+Yv=$kQXk6^0zXR>h(d)d>-T+?vVgOu6Yh1~e6UW}nL=DdJVh25ynd2uhQ zw?+zCP88qatm71KV?pMG=~+u&yI3+``Aq3yL$mJ@H@$g~ZF>iyU3><{X1&UQR+wtm@9lQ(!r7bVUFqWC-JX^tY-_YpCdSCgU zKsGa3MY{No*-|g?YDD8ez@85cP3#axAamPQOmDsvs6b64kie55sXS(-D_xb0tgWkF z25z=rF=d-H9lz#}NjGqOmkmhaZP02NS5E4Qo6TOJ<&Ce1tLiSCpEl-QHL4sz><;I7 z&uAoy>Q5E7ZSHv!8R3lo$;ZDgcd=?zf0Zwk;OCdps5c#oW(K{a8bi<>`n=UUf7(7? zCmCro=RAh+sZn^lgHR>FzIM%x1mA>aTRXYrEJ?$ctSUVQOSL^l>?X4e_qvT&eCz$h zaO0KMc~{9X`!nOR@}k5n-fK z$yQ0oGBQf`En^#mgsd4^GAI?5ecuO#Au=R}v1Tw?24fw@@_mk$*ZX~bpL0Iv^VhF) za+rBOACJ2|?&IG3xQ~0YD512sLkql@O`{Pl*;ZIhy=S<)pKH$FQGa@Gb24U~vZPg} zoWDAu_`ZMgw#Rp{`=UMJomyUN-q^!*MAH@Z;JdH$H&!gHUXBoIqiBw1Hwiwcs>)w% zXQ*o}O7ETK{<|^7$t79-d@HH{U@(1v-Q#QSR?jERcjZ=vN*rCmLq|WPFP#uAPqki| zwR78V8+{4mB&B^CNOt zk>1?oe&=NEhK%x6A@~bh3NcB5)m|k`Q*GQk4|jQyx-3vyYOXG}Epj*eyHu%y(X83$ zEiE_Gl@%fm*FVrGP{VQEpUW3@)e(riIs!I_u~f|KWq1$YH6Y_?f;P~n8?zmlMie%R z(V!4q-#hbla{gH_xAXybK}_Jj@Ex(+E8~(sKRo!ZFg)G&F^cGz8~3KLN|(zPA=lPc z+0aocl2CS!y-UXL(sjM5;&FB{>j&hD_3u|K9;V|!YetB)`E8|jN>h=O+`RZR@_R2m zF4Zf+8>NExJ7r3u*1O8hzft&cSDQ^%`Eb&`7OlO&zxc5Z!`Ic=X9H!WBrgiUzVKu;0`GRhS+;)keRp{g|7*)8RI;; zUK!eocU>n|)chujTB#f0@Q54(SGX_UZP_=fU-NsYw&Am({}7O!YA*M2*Dl zTR9N66vco7NGt3y0!?a4U+Lf#H6nU!He`8EN8swW-r?PHNOx9_T*|fE6wR6jmu7Fs zU7ay3yy1SN4QZ>M*y64y8Y6x!(sMxn`7rGaWIDyds_;tKx>x(T-cVJG1oFyt3NqU- zEE)+iNRN6=^`ytw$ORx$<|;R8WcV&VIhzE~waJZHP#pZoDcN@gv}< zEk8HoiGuDGV*SG=5&bh)#CZFofoi!0SwE8o!`R*0o$OPj(HU>a2NP+bNHv)Va#GV( zhjgzV^uv|)zM*qTvKe1bh=+fl9?yUji{dw@FX;@7Bb6tqM~ZVJR>8!!zT40YUj0{=rXUNe=0wdqgKfG73^=Yx!+c%tv&92D-K-u#@Ok+WZO-mh3H(M4vaWe+}{8eJOEwxSL^u~SrxD6TZ zs`HcFc@4(xdE!=}7{65qw31k^0Xdu9eXMc7o!G=9=suJlR+78b()03u0LpavyqJ%j zfz^AHX`h<$ipX&mmFum9?H-#TV(-;e=@W(NcYN06-^r)la;|`gJ_uUv>Tvv?kksS6 zv`O>jC`$uKVu(G$|NVBrV4~EImALXnGf9LYr0g}k*j+Erocstb4w~sN%90fY-gKU|<4PcYj^@5Fg?BvKhTn?r%Rpo!`HCmTQD4LT#*we14x#SgQSZztiDhxw z+o#VaJWpCR!QC*JzOFKV$^B8l%;0(GGjbX3wrKg`RaLGTnKQLMYOSQ%xkS1ZMXXm% zj7sa5JLv70cVO(rh}9G|5?`n|Nx823T@tyvmInS-=HGss6UOIm3x>b(P-$x=eW3J} zuN}q7tSg@@32o`q&MA@{vd^CLn@*B;)&Xl((VAE6jw(;J6*#E;=ATTKj`5eqj5O7r zs`0Bsi|(lsJ_D>)qC(czX0bk2y~{(9+M?0wCFiQ6$QaBBzH5u>)oL%^Him=s!Mw4} zIZg}qu;n6%>kPV8aWX@3x)-NGym*za>V!AASzR(!x>QCSZW@S=DCa8^{32TpFHWeU zEK{pW=y@ao`z)Up{>JMZLRmyG(bMYV+Unl*)NR>jS&pxU@D6TMzslO>A$&Np!=x6 z`Q02_RiEOdh7%$E^5*gEEt1@nWFz8h8J752HAb@wX7~?*zP6fYPuF})k-QEKC~Kr1 zd+zhqKN2|OeRvMotS8=)1i`v?T@}kJAU0Tjm;Zfn!WDQ|hYwnkbPzijP^<5sQ=?1e z@l}epCY!b4D__@Y7soh}%c9^O4k<`Ob4Jcbrxp9`a#r8vE@%5ys!vwH=B>07;`8MkNfaV#?Q0wgl@M!Do3`BC6Aib! ztCs?w`EWSRsCOqv6+)I}^&=GKcnWbIc-W=I*JtP z$Pr=rt1*+;b?8#cbCPAxCa#%L;?zrg>C+j0FN{0RRok6A>vXNhDV8w3WxV2l-$w*^ZSeuDchOLx5|6Nd6b{cE);X7 zyil`1Kk8cUjePM+zpFm$COOFbk#ATbo014_qyL*w2I*prW zrqP&LXZw1YN>L%Zan+SXA3NV@!V65MZqCuQI-cC2#ti8#peF{=!qonRlXvBOCr8z~ zy;>`~pHg!bq~$+ct_<#HvaK!Xh@Zi8l_wQ1K~heH3$C1bzzvCEQEz^Y&bJ~b^}lmG zRmKU}`LF$WW5_>|NhEI7a3hM;Ir0a%l`at^=u1k9?;P8; zg>l+Aa$yqpz-`zvbj;D7T7#)$=IfnZj-vMUT^}yEvizaXwEzKfCn~y`Io1u_b;4<) zXS2&Z)Qesyb}CGcm44j*Xx3KlC$k-wb&EJtmTq8I?dTnKttKjjbS_MLh%0|f6twiw z_*+|&-HwA=_|FH36jCP;c7FIbUOwKcnRNTT(?H-8%qyIhC8k{?Urhg$xAFs@d~>yf z1}gNizOPP*LO%iL*%9|IBE7^WIYn$)Lhobf_|sh4^F;gobxqd;QjHhhJecF83yi(1 zd*~}P%@Yt`raOTq*&)KWCAjQ4r#-B@Ht#y1QnwjLi=5F9D!oEPStwD z#wm9>xmHVu6RoB8*!_xXU0gkJ-|eUvXaWCv#`?o5PRqyKz%iLuw6>&F+HIh4U$qq1 z3D8D@>eu-w+`B}$m9FlT*2 zg@h{JS-vF?3%#!6_wGRpPjv@6m;Mqn*!#{Klg-;WrmWZ!>ZtrJv1v&u!yMO%t=WG| zs$6bhdD>4bI*XM4F?2g|Pw8JhqWZc8ozQXllorOt(C6b1^hdi4A#n}hOe-hxc&B^R z8GQt$@492-%ho)KfxXQ^QV8vB&f+@niPup4hmFv~Lnz0A!Ax{E#XO@f{7r{3`+2LD zgVYRr3-!z@?YwZrxZhKM?cqn^3JN1Q;!%NsM2kUdWA`fdMTdY#=^Gx1fxBy)dO7E> zh(gKZG+9Ll8|wI){{}u~ET(eUVj~_iHdDqkROY4=1nm=kK*k1{FMds=7ag~{D(HBf zF3{!^XM8HU_Dvh_N9x+jJ#|}2h1>A`v*-S7Yr^QLxkgR#@3{f7&;gMTcgjSCtPoWb zQ`S1-nxguMcTjZZ{I~s;_uXD1YhK@@sOSu;L5VVh3X$dwC zY|rjK?V3RTtLUoKis%k?LvW2k+uVm9j zF0cIb^gI1dMvVW~Ualq0@|qk1(uoyff2-Np4HX2z&1Y;qxvN1$^_AiWdSjlCH{OjKNGvmtxA0H65zvhAJTDpzZ&&^#0+*W0Jkoo3x zo8MEy1>5fNe-JIg$Sr{nYYldqD&H5#c_rD+$5&1yj7;~~UwC8>SrKV_?D}sU&0B)| znNnp7I`xa9pd-1HB$7#Cm5ahD+C{)0m6qzZ;N{T3Eow)dIkcZZ2`57LEZ#)&h?+%1 zOqJF&DaFm3N^0eVPe4hzBC2rr ztzH5w>pp6_4e=b)Ox{ue;-l&LwPq`#zOw#NJ@lK4Z_Nc}RDDb(`)d6Y_H6yd%=_X! z^a;X}^(nfX>tmrs5m-J9RO|?f2=Tuk*RxDPLkkC7b=z_mzos|8aa}V>J$+J~O!O~t zMEK@%n$z>#90k&1l5lNEW69>*(_Za2yRe2AEnak&1=6rjkAeq_rxFY-Tt0M zB;BBi=!vP_bL%3cw1(k=^U!MPmlfhR+WkLp6HOdCWyrQp$&Z?0UkS~}tJ4a^-~#%q zJf09E{Vo@?%B3;x> zjzlJs$e+;t;<#1szNQt`lOI;>m|v`jyynyiLNUMD=E@e+SFCJBcKVHxV10Mx$NS2T z+uxrtHZv-GX{blcbe-@KCg!b*4m88pNidc!(hagbTvP~=;b~=9^X<98Mn@^S#QRKX zonyCs_Sn`j35AGwuK|Cdz|DUpYSKTCY4T~=t~1a9TS;#Z!69`T5s0e~Lzj}(-oJ35Wc7`;mC>IKVJ&cO zr7^MmKv3T}t0%%7lK2)^*zp?r+3YKa8Hy_1hh-ERN!c}2Vr#zu{Z@|l52Ly?wf*;v zcyBHNK3?o6;rSUh>EAb4FYfj{Z3+K-m&`=I+MAJS*3vr~zy#Eph@hd>*U`m#$`kSG zHQO6AJL}NRPf)XaKr#>G_J6BrqS<18f!~~aI=y=AgmvQMnQCD2eiqA<6W=#Y}w z*f>}>X77)nL;a3`s|>*MqcJ4@b0}wu=UHW+<)7n{@bxb?`L(OWtBaEAj3wDGO}qF- zaLy&vy{?t6RK;+{?%>iaq?m;bapq9s?94H-b%q1AM#1X*Y?e=ym(u zYpxZqcCi|&&j~pe%67*3+-^#D|Ak5Ozu)OT8218;=PTP3p_yV!{p{k-_> zGYd~q>u%Pk;sN+IEr=or7`Wh79L&o_W4ARce<1Yj;Pz@c{Nc$apphH48F=COuNR)= z{>NYiJ-;U6{xuO(_P+q8S{wcXKC^+5+S`BcPHpVi@8MM%K7wxv$h?o$4Pqo;76sET zUf(Dv{1$SImF>(_Fe~^r!`v&91{l=x{FEe|0i4^A2ffoKzwT|?H-m3|3yq30{%@+Y1%||va4|b%cf8u#1*>l^OdhImhIi0=cRX(qy;t4)Twf!>p`^JVyigZJ+XRl;vf0)_Qbl) z345m+YIE|+DrI18j;y}34pfk2tUT*8U^t4JKQ~ghBPEZkApQ6p3a#l*o%=`9Sw^OI z`I}_4klMz}xBi-He$fzh|0Yea`Eq)@h_&NyDPRm;J~(5scGi9dwOX8>S~JQ59i^)> zgsdzz0*a-%UX2!5pD~olH1iztz*qdth!}Q~xrmJ4D*Jtg!~nzn?}Y@$Qf2u?@+0Mg zZf#!}d^u??KII4Ne|Dru>)#}0;L|;SB9y!Q2MWP6re*vNu#)LdIwIo(DJDjSt99^C za|cJ0-S7KtebR3xFmfsWzt8;{1oZc!3}e(^<>2~H_>3I2xU)M6^5w8i{QW1fy1{>Q zhyOnJ8t+S^5u9#do z(Erc>4dwp@6{CFkZ&c!j$>`hzCsX$QNd;yg@IMjYzY+UQ?%&`mh7C9MCj4K~{0(E# z|G>YN{aw|g#3BaWjudq-!`?`p0y=N}Z+!k=kOrHX{vY_)XT!)Zuph(kY~t0ufPz?| zFO|Pw{pe0rp8@^d*)F}*lkdNh2`s$(4{EpQYXLvK@xH^K+jlLmZ6~X zDNcSfrXvq_w!bQ$T%b`&=y?=bWd!@Lunq3Y$1irP)!RtsLjSw~o>#Sc>%Bz&V`V_KT{5mH% z_3n)y7Z3Go{n6df_b$Hxn6a(~-v*{a?%qIY;V3l!I~B6v*Y7F6mN#od&=)Q7)RMAv z-R6sj{I_X+g^1rk5rMDxY%qmoEId+g%{+PxfzMSqc9lY?3E?k*+&T8U0HS{Z$dqlt z0E&SYGR|-pqF>S%d-$H=Uns;1KJFr{yL*M8NW;N{F$O~j`$fvC7#J3P$gz_Pi8u=g zvGDdRetR}BloR4l%BWim4mq}IhwplGHGT6F7#sWhwFA4XwA5hOCFTz`CW!X=DSq@Cfc17!RZoq+mH zX$R0|i}C(jIV?QzYwHc1{tMp&)}cG1zxnyRW{fA_YsJJSb%kcacEFO~`pO4=HFCNC zOgZegaTql@76J8NS0-bY`j8qdY!ZoZ|8*6as-GF)q=?wuBVe6q)^KFIYX*QsqMm^vw=zG(rOg?^)ThR*<>*<_EgQXs~6u992o z{e`;OqPENPsfEAPS*w5w(jjg#MtBrpFOQbHR^h?^GgC{8hEv|FAN=U6Lvd-Q0XtF7 zD3UbPpBh0ub=dfFW&c}qOVm{tJ6pkXe)GO{w9eO;Vzd>3 zT}EMy?F<>?-Z8$|1||O%VFm0fX2zNgUdGT{-X&UW4AfEi>!=(_TBw^WMV&!kR3jt+ zQx<$P4zu|!5xWde^&IxzIXT6k;(O_1X!|z2|otolQrB%ktY4uX8Wz}V+$7rXE z4Nm*Cqx`}6233B#tDt7qrxL3q0Zpqthw zC+ap6cNQfXYBb;!V?c(60e)0RqxH^D<#GD`yoC3q*u&|H?kgSF_{?u4mn8*h9&0=) zF0QDN&#pstsh#fs^Ag0T=Y^3)xP2B)_z>t$fedr4FY-%YV)yAg0{waE zjeoM=f#;R)Rg4C)6(qSS9qd&vcokH71kOLj1vXHXkjbP$X#(fBmb(L#9CvBDOAtL? zVBwUozCGNb)3Ct`x>X>=96lq$bIr{v4*K?0pM;j4A7Hj^wXt#vsqXUSHyOqE0S2b} z;uj>u$Pn{Bff? zFKFo2T~4vP&}D;1OMW1 zKshi>Jh|OEEr0$$?K1K6t{V5J&$skh9RlC@O(;N98ZCIlR`y=ZG?#A*5aJ`~C@&VP z(~H6H{tHR?RuBBgO9_H_gMxvRPLO@(AjbNG{U6_!0oU#(_5`yqIH_2ktV&kKAN3yH z3wda95kz{6)_aKo7NamZ%{R}xRj+}dz_27m{A*chMyQ427Mc%!N~=T9Z4S`;@ET=? z07$g}#=N)<#`Wn>f`xBxONJ$v=W4%~N}5hy-S$Rnj}ct@ywXfgvDjJ_q4sUMv;R^G z8XvH|1UlJ~I`=3G=xV*~(gg*4fL2!6!k(_gJ?Mu5_5z+ROJC3FW7lfub!Dn&&la|Z zvk2Mie4lp-RY}U4tRC5#8}ClvE^#2RfSi3w=2?8B46%6tG_mRQ%5?fvNds#YM5H00 zfcAwGI~Sb4F)ALXv0a59jF3n)b33@ZAs=L@GJTsoA08%5|HQbNL}>|#c02Ir9FDTDL=Yi$vy zrM(>Lmft~jVe75>FGFXM1&5Y51f8W7p%EWds>2@`;HAcfBi)a1P^HM;}7-J2x8HgbqIH*7bh=4j1kwwRT8p;1mAr zr(`%HhA;$(8EXFlkV+k}mqcLCj9CqnVQZCL}R;RFDKYcWQ((}qxOelu~F5mHk?#W~RixPeP*JSz8CD5G#&i{j%tOB~#n zkA6QEglfWxKT5j%V}>PrKX-#ifZDyD(F!RyB)+J`=6IvM^{falIB%-(49J@!XRPmy z+B9(Y6LXXcf0?iPj-Kxz(kGA|(VTq7_2wR2KluD4P>s`6?at6SAqe~@@lZA|7)Y!U zY&)Mu1^`R2nS(De0tyZAB|z?^0VqgP+>f>J*+6IHIU`o#7_Y`tVxE<4=9hQIz;#ON zqpa$uefB|?A!`-5)=)pp>Q1;m>XUE?;Oqn}CgD`CEPK|@n7i#~A#SzAY+l=b98*}c zMR88YxP6g8L$45iYmT!OA!V5O( z{8W*smT+`dzT-wc{0Hd?J7odvEf9=HP0GKqk$$Y1wig?dt+dJ!J;AxNz^U)#J222u zdSrQc4eMDVLu42siJH-njn|N~`7#eKxJWfePt;iEtN|X>Wt=b(h<;Q}uRKFgX*hKl z^!BLNSYPf-G9Y@Hy4LJVo3}W{hV)Tm+)K<%(Wjzq+U^LAb4)fl8 zYPw=NN+3$(e#^K(Ex}cr9!o9|__VC@;#Nv%u7EZxJv3YnE7mI{Uh??~ClHy(xRgpQ z3NU4>uKr{zm0cGcz=LLuI3p)){3mU!U5kd`k$irm#x&vaB1`Hwu)gdAsXV3aB-+J8 zASEBRR23GnKHj=+Um>Rz>d+T=U-NwX?@jJ|^I!CdKUxlMy1Y|$KgKYME&r6N`T}N3 zP#hAW9^iAr<`?z}IQ658F9Bz&=z)vwVbp2Z)+5aFxnu!#f7D`WzMt;0Pfw?keK{35R&Hrj%Sc^7oeqea*gKP7X92QuaXes#XW?dA4%cdE z2xfJ!mdvlYwRxnkLRNgWtr8HgRPhD`!d3PrHT-KK0#{{IXo+DMt(UK+C!GMc$Po+* zu+?Jiah4F&o`Vt06^e&99FxhsQGtjbXdzW3FK5SKeUeQe=6;N5z3{${^NU)7ZiS~q zsR(wjO_#u%-!(bXptSS-3IwxL>(2qn;~n@It!7nm^5zl3cEh$+j`5?VtxLYN zLT4|>R}c1aGZw=0N>K9?>S$#!Tf>+_hz~FSDo?~cz)PtdmpLS)B4DJx--}{ZLz}YS zTrP;=!*SS1<<3Z2D#{YtrwapaCQ@6?8KTWEBL0&y?LE!ydJLduIH&V@V z?Q*Ws+w`X3`7o~)R%?uVDi${D`HY!dH@_V3c+k*)S_#eW=<3K{1M1XoL&EUDetry8 z``{T?8()~T2w3Xq-T1BM=4WVqqd=A|DcF_MjHz3kN|O}~P!Rbhb8J~pFh)5^%2I%K zB&K^_Zodw4K|>I&sRMKQqB`C#k8elRKN)3M9MSiRzUgfPDw~BzRuRM+!IZSgq|*+g z_v4=lHS9xDHGlHsm?G4YUiksL*>hJ?euw|ip~zArn>55f`^8piGx_=;xuc(~ zS<#B$?P4^nC2?Hb5>gkv>Uia`SAq`mt_lVrjW_G%aQ$LTTb5%zMJ?`3In#Y4`^)|b zT%el;r^CV}Hk$Vx!8@XOm~q@e_WqXX#E@eGF%Wto1WgKTJu9<6(v>84L0aclAyY7! zORSv<@mT~ptDwj>3&qa;z^&?eRvEywnGp5NEdH&w!z|JFfx5y<*lz5Xo}duOW885( zzuDW~e|ceysrS#=N*URl+&tgl&Wx8EKgl|}-j`ke%=L%&GkGIfkm{X)rE>wN@+DNS zp4?VAsp|X#w{*IQ8}AO-#9y-ETXq(Y699*q2UWkuiKlhWat8{@Jw;~k3ur~PnGOr1 z_t`wxA|eA@*G9G}BVR5*i1do$m3lNI>o(D_62$S~p!kzOT90m-5GVE(4Cxgi)!BjbY^GzO;Z$Qg_VcGWU1mTJ-^%*33>z_>!#`baD zx4k;ZtxHQE>){pyt-Vex5zMY|*<=$1FLv2hJ3OuaZ7bMtjBlSQiwCJxaBN>5tJr-d zw)%$tW5vcz?v}lUI1#+~Q!kD+kkAFQ&IS_>wJ~8fnG)5*nD9}WiCjbw0n+ht*9=c% z7J|&uc^`(7gW)bd;`N2mF;VH}#Rmqc#N&uAELGrB5Q+Ca4_Iq6Ct5REA&(}2`}Cl? z?`#CcKMim(P0}jC20(BgbSUKF>zoT?@G5Gc5Zu$&eN0~*5jWUu8ooSsgf%OSbe^de zb4R$=OH#TP!zc=25xyL4lhPf5=R#>R!>7Ey+FKY%n=$Dm$$B?yN*Hwo)HLtK>ILF* z62YP}-+l-e)e6`=C)Z1bm(Yb8wi}I)p+rA5tXL40rkk5IRY8t=@m^nfF>0M{$zne_ zW8UqQ)|uygngOwi)|MR!n3XM@YXU#N28HPs%yXn&t;LvdnGo#lb%cvInh7=w9vK$V z+NdbYqi+-CV=fb2fB4AxHy=zZuIQ8k9Rr#{JFG2XF225Tx>V}WZreS_{b=Kzf+#Jc z`A17XKP#}_X+57mU*IIW=aY#FVB=B5Ilb!l7Flu?nUCgwQ_3bWLaC!VJdu=}F+TAo zZKS&Qdoxq_9BmNknDW~?QPl2knT&`zSfdW~7oMFB^Gdj&s@)VGu!U<}X4vqz!)?%w z2xU$sr-}zG9cP%lYDQ?y=e{6>z_$xJgSMQ**?5;=$e9z-dzWRd3owKaxgwZux}*wC z5CrFykBwd|VVDFHK}Fjy%H_(Iy!8M>a)u_Jn+$&C*DRmbz}5-WBREvA$xMB{&M@it z(dRb51P61@Z}R{h%zN_1fm)}dpJDYZYOcK+K$9`Gg7+j+BizGg%W7XU$DZPi2Ds%G zQ7Fk}P3FMC*iiU~1|#tWd;X-xv6-90U=bOCQQ_y5M%VtW^!*@_1^bilLk^;MH2J`w z=C_Bz&myJVqzWrW;XNACv)xiIpv#*dk7Aie+4umW6g)%3upHA7eja)d0^%kaos`XF zRr{&ohCH-c;lu*ECtOibn4UY~k*Q@?(h%a{>e}JmplQ1GDVLX1=D{Mmd54EJhfV!8&%+fblZ+5kv?HLWHtvZUOnQ8ff_ISG0!&690z-> z;6SCm@O0CriiH=7QlFa4q&j{`s(R(uyMIeJcuqkvq@Qn*5U@q4VFEm2hx1hEAg!Ll z?fRH=PLJ*pSG!;A7;tW@E}^b4ZF7i8>g>IYX)bZ{z+o}HidcKh_y+zGRC|-yR?~P; zsBc=%q^B@Oy)`0y-XMMBdn*aD;H7CE zuQV5073-6)QXTfs&ZrDiIZST5792SSRvk0BUvXkuFR-;N7|{#UQ&}exKVT8H!x`Vt z;6gML2zA>l!7Q>;Za>yzqdPoXzy^>-Q;M%M|w_&9C02uhK?+AT315p&1zT?oKznA`L zUvxj?qgKFulu@2xM)QDoK3H|qwW&pPbttd;>JT*T+5Au$e9zr+<1uIljnCpSS!3SF zXR^I(1QNlLOnwyGFYj2dVd2up3tS~!Q2Lde%t zvu&$)&Df0`3Q*K`OzIe*|N37xcJH_<9zrF>ukZUgFtS~yGfqUR0U9v&8gv*9{J)Oz z>TuU+yvBdY7sJIV8`zjXWyv)#sd4wg#PXQw3#Gu>rcaF`Z>J!JbD-k1W4^iMa|;2ksV1CAG-m_^*p+~Wr8-hLA`o7v zOX9M1Cx+kT_tb#E!dYrDz|JA2l+BFJv5>mHnXOoFa>lVk7{{d^{;ckq3T;CVif36F zbIx)6!E!|IrmOb0n3uoY_v6^WEGd|`ddn6DGB3*$%1yTlp2wH;L^Zbq` zAo|yuG^#u~T1tc|4p3FIZk#@L9S#cFb*?$M^w$B=hhWs-lJ=JFthkAXb8G8#j=^vG zjzX)bGcvT~(Xzw-9}W=29*RSyfwj4&(~b2Fc-I7ls{*m4G!uK11@LzI_EdU8`pKxe zNUnx`hv_7Gc=&zH5#zVG5(1S z8c)5_;10n@C}}x1WIQHZ9H|g%DZ$EBHtXYLHU`6N;w|e6e}bWt#~YMfG=>zTx9a_y z;6``ByrXlkHzcjF-$~fck*i7}DWOQP9l!nRfrFciqQ8 z&MjK!VOfozx@kb$+rWH^C5mPbm5B~xVbQrQYwNxI1|kg~3WHNzH>D2hobKGeMY^2Q&r96nbS;{Ll z1IzxctNeiVeDU!N(K6!+R?fT0%kmOx?1|P9?5A(OvmmtuFDFZ@$hs$ce=>~7cL8R! zW0b<3SS97pn)1G2HS=*^z=(&a`8;y#zn|DjAjGn{m~+i~9>Z(}&!9%LiZ}(&ix3QK z1De6~CuLqjdjRv%UT?I*(&1q!BB*eTVC*8sE+md>VnJZQ0{q`s^J%YOV)cE_0YYye zs);|=W^!I}nk|EI?66rVag^G$;vj3{yk;xpl9LKV31L=O8+Z%wyv4!fJ=maQVEa?q zz*T=hIJ0L}U|CfEARN1gE9S^&L?|0G37lBNVezJ-z}}3BIr}&`?K9wD0ihBnVemX8 ztcj%z2(S7Dhu|qbzcx=G^#=Kxt#f*3pSx71nb(hy-Q*XZU<0h&k9t-wR!A6C^2Ab_ zlM-IU`+4}IeOa$?oU615`9}xDbZH^6$e`fXV)CA!KOx)?(!DYQbXH}LUKXGa%4AyZ zPz_I`diJA_>R1&NJPucmPS!_KUw|S%uQ|VnkVyJz88i`Ki{q8mS`NRvIU~Q(c(J(; z|3e0-D8Kk3zh#^g$KiL1lOEN|EoMUX4&G{(i-MmDlX7Ff<2`$GFYbut^L@%*HFC+C zGOPG>@K09UUV0BdM6CT`@D{GZh22%)283IOR+zr+^%@e{wewA#v(Hn&bM_6?dYu$q zxL&vhV_KbZboNb&36r*o&Y3B9G#=OX8xkrelBt zJaBTRVTUW66MqC%-wbynvq0XvW-=44ik=;0*mH={V72XxXu0O5y)>#pZY}3R*u76d zbuDk4bq>gSZ*LIlWZ>`y0_P2GZXun<0;cX1RZ{zY0nQQ}O-^oaAFw1Nl!b-XIJpH& zBBZK!X!hqp-UUtmRvAwUmHNaZ1-VacmR4^`y0@Lhap76a#*b}{RR(GK?ZX|mIs5gc zjuVYPvIJrp|w0-Al(s?2s7V3q!3K^;V})`SwO2 z0QcHc$7PVZB7)M~hXW&$nm$^Xb^yx7a7B=0%=CF?Ukrcq5wOg{;3a_wo$d}A!}hB< z1$LC<$ltSI*62=3okwNW?9VI|+;vYCB4jcfHG#t%3FGPJohMo14LeuJ+f*Qwo6-&V ztbma%mPAtnT>OrRgZZcl$@|PoQQ~S-K&vRv$Hfj0bd`Yp>Bz-9P0;Nz=$P=MMA3B( zF3easr;PFKhM;FV?}VWrX@cVbWy-Y_%OW!VJu$SU~zN=J7x}nWx*&Binc05DXiv*vcX{&34D==@-L z$RKqQG7r09SNVQcTzy?jnZ7RwP793rotog-PFF}bNL!50+D&w13=dFK`G?nM{^5d7 z0xDBFQ$QyupDv*1Hm;@b^UZyYGC*G6rTFd-@qNy~qcVCaGjZ*uNoQ9T(vwba0D-o= zBM}U)>^?Kz*a+6&&vyE`34Dpl)|JLE?N@P(v)SzbICaGw5m574_GO$~!C!m_4hzk^ z%Bl;_QkYAInWAFgu1!3NpR|6D_~eLNh6|86U|IW_aA7~2xZ0S)4Pvpu=1Yl+1C$@d zco=U}KF6#*-+oL#UjA0x ztNO3=K7?wRJ8(`lFAnrE`aPdD!M$7eocUVXS`GP<)`~)Gh1l5nh1|}xoMg*5v|g& zF&+BL80dRi$U}X`8M%A^CBRs(P6vi6*8&cSeuL1GH~vL3e=}=jXKQ39z!^w0l0=6$ z9YZy-x=CqmzQjK+Y>M|nK7+mwLyez;tSPrb8S2>pD4!s7D zH}-wePSs4F%Jv+v+mjfn3{*x0zG?`F+ENux{Gs%d*r;(J&F88*V31)+YyR_V+p~1Q z3G+nHd=$YZjn$2l2-@K`@7O`NQPf7)0sJ)}oJq4CH;BW$9A6f(5mWv=zUX#F_UCrJ99M-$mA&zF}@vmA(S&hxg9XS9 zF_vlo620_K0An7e${UPdEY)fT8B|pKJC0$e3=(*~u`Z8flX;=QC5Yu1+_<3^Y`Uof za^^&t%Gt{`fiG=3a7)Eo0x2`%x%Dx$oImAz0d~iFjIy0nDsBW{1y&mB9t$%BWExAh zd8=c7ZBIAoyy_&IF!sV2o0zdGwb*bo@)-PDpjWkV=pTxlwC-tSIRb{CXs`mr)bhtZ z=AkV%>9>zf*aTcp?i+*CsVv;^3xI}ABRM_>n%4axWv`~04wWU&>*@8uDw^;!=qnmB z-W#Mm78Di%O`?6eUUD*xeEC0Cs$e6_(J<)y#*h~eeJvc$p}oP%#E|EwU8zrgX}sNm z`z6M#!k}>_5n{EP$BwLgkgDqJl8LLJ`0&U4M`D4;7@IIfIGio4s+(wF^5FFw!UN)` z-81dqz!eEbNY9#T(T0(bN7c3(;PlmOQMtyBIW j`2XjhMgO2%cQ~{D@!xuTY)6F&_@||*cRBB(Wzhcve^$A? diff --git a/guidelines/srs.md b/guidelines/mig-sadd.md similarity index 77% rename from guidelines/srs.md rename to guidelines/mig-sadd.md index e32ebc444..72f430bef 100644 --- a/guidelines/srs.md +++ b/guidelines/mig-sadd.md @@ -139,6 +139,7 @@ MIG Storage SCP service can be configured to accept all incoming association req | Save to disk | Sliding: 250ms - 1000ms | 3 | | Notify MWM | Sliding: 250ms - 1000ms | 3 | + --- ### DICOM SCU Service @@ -279,6 +280,18 @@ The following APIs are supported to interact with the ACR-DSI API: --- +#### DICOMWeb STOW-RS API + +[DICOMWeb STOW-RS API](../docs/api/rest/dicomweb-stow.md) + +--- + +#### FHIR API + +[FHIR API](../docs/api/rest/fhir.md) + +--- + ### Health API [Health API](../docs/api/rest/health.md) @@ -291,6 +304,37 @@ The following APIs are supported to interact with the ACR-DSI API: --- +### Data Plug-in Engines + +[REQ-FNC-06] MIG Data Plug-in Engines provide a plug-in architecture to enable customization of zero or more plug-ins +to be executed on the inbound and outbound data pipelines. + +#### Inbound Data Plug-in Engine + +When data arrives at one of the supported data-receiving services, the MIG passes the raw data through the plug-in engine to enable data manipulation. + +- For DICOM DIMSE, a list of plug-ins may be configured for each AE Title +- For DICOMWeb STOW-RS, a list of plug-ins may be configured for each virtual AE Title and a single list for the default API endpoint +- For the ACR Inference API, a single list of plug-ins may be defined +- For FHIR, a single list of plug-ins may be defined +- For HL7, a single list of plug-ins may be defined +- Each plug-in is executed in the order that it is saved in the list +- If any plug-in in the configured list fails, the data is dropped and may return an error to the sender, depending on the design of the receiving service. +- Plug-ins MUST be lightweight and not hinder the upload process +- Plug-ins SHALL not accumulate files in memory or storage for bulk processing + + +#### Outbound Data Plug-in Engine + +When the export service receives an export request and downloads the files included in the request, the export data pipeline passes each file through the plug-in engine for data manipulation. + +- A list of plug-ins can be included with each export request +- Each plug-in is executed in the order that it is saved in the list +- Plug-ins MUST be lightweight and not hinder the export process +- Plug-ins SHALL not accumulate files in memory or storage for bulk processing +- +--- + ### Storage & Subsystems #### Data Storage @@ -327,41 +371,11 @@ Each event is associated with a specific type and is serialized to JSON before s - Workflow Request event - Routing Key: `md.workflow.request` - - Class name: `WorkflowRequestMessage` - - | Property | Type | Description | - | ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | Bucket | string | Name of the bucket/directory name, which can be used to locate the payload on the shared storage. | - | PayloadId | string (UUID) | A UUID generated by Informatics Gateway for internal use. | - | Workflows | string[] | Workflows associated with the payload, if any. | - | FileCount | int | Number of files in the payload. | - | CorrelationId | string | For DIMSE, the correlation ID is the UUID associated with the first DICOM association received. For an ACR inference request, the correlation ID is the Transaction ID in the original request. | - | CallingAeTitle | string | For DIMSE, the sender/calling AE Title of the DICOM dataset. For ACR requests, this is the transaction ID. | - | CalledAeTitle | string | For DIMSE, the receiving AE Title of the DICOM dataset. For ACR requests, this field is empty. | - | Timestamp | DateTime | Date & time in, UTC, when the payload was created. | - | Payload | BlockStorageInfo[] | List of files in the payload . | - - - Definition of *BlockStorageInfo*: - - | Property | Type | Description | - | -------- | ------ | ------------------------------------------------------------------------------------------ | - | Path | string | Path to the file located relatively to the root of the bucket. | - | Metadata | string | Path to the metadata file located relatively to the root of the bucket. *See notes below.* | - - Notes: - For DICOM files, the metadata file contains the serialized representation of a [DICOM JSON Model](https://dicom.nema.org/dicom/2013/output/chtml/part18/sect_F.2.html). - The serialized file may or may not contain any binary blob depending on user's configuration as defined in [DicomConfiguration](../src/Configuration/DicomConfiguration.cs). + - Class name: [WorkflowRequestEvent](https://github.com/Project-MONAI/monai-deploy-messaging/blob/develop/src/Messaging/Events/WorkflowRequestEvent.cs) - Export Complete event - Routing Key: `md.export.complete` - - Class name: `ExportCompleteMessage` - - | Property | Type | Description | - | ------------ | ------------- | ------------------------------------------------------------- | - | WorkflowId | string (UUID) | A UUID generated by the Workflow Manager. | - | ExportTaskId | string (UUID) | A UUID generated by the Workflow Manager for the export task. | - | Status | string (enum) | Success (0), Failure (1), PartialFailure(2), Unknown(3) | - | Message | string | Optional for error messages. | + - Class name: [ExportCompleteEvent](https://github.com/Project-MONAI/monai-deploy-messaging/blob/develop/src/Messaging/Events/ExportCompleteEvent.cs) ##### Subscribed Events @@ -369,12 +383,6 @@ Each event is associated with a specific type and is serialized to JSON before s - Routing Key: `md.export.request.[agent name]` - DIMSE SCU Export Service: `md.export.request.monaiscu` - DICOMweb Export Service: `md.export.request.monaidicomweb` - - Class name: `ExportRequestMessage` - - | Property | Type | Description | - | ------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | WorkflowId | string (UUID) | A UUID generated by the Workflow Manager. | - | ExportTaskId | string (UUID) | A UUID generated by the Workflow Manager for the export task. | - | Files | string[] | A list of files to be exported. | - | Destination | string | Export destination. For DICOM C-STORE, a named DICOM destination. For ACR request, use the Transaction ID in the original request. | - | CorrelationId | string | For DIMSE, the correlation ID is the UUID associated with the first DICOM association received. For an ACR inference request, use the Transaction ID in the original request. | \ No newline at end of file + - FHIR: TBD + - HL7: TBD + - Class name: [ExportRequestEvent](https://github.com/Project-MONAI/monai-deploy-messaging/blob/develop/src/Messaging/Events/ExportRequestEvent.cs) diff --git a/guidelines/mig-drd.md b/guidelines/mig-srs.md similarity index 95% rename from guidelines/mig-drd.md rename to guidelines/mig-srs.md index f60c08b59..4c0752e3c 100644 --- a/guidelines/mig-drd.md +++ b/guidelines/mig-srs.md @@ -484,4 +484,23 @@ Setup notification service, make one of the dependencies unavailable, and expect #### Target Release -MONAI Deploy Informatics Gateway R2 +TBD + +### [REQ-FNC-06] MIG SHALL allow minimum data manipulation of incoming and outgoing data while data is in memory + +#### Background + +Accessing and managing large-scale medical data between storage devices or services has posed significant bottlenecks +in medical systems. This requirement aims to address these challenges by enabling users to effortlessly manipulate data +as it flows into the Informatics Gateway and just before it is saved to a designated storage service. Moreover, it +empowers users to perform data manipulation at the moment the Informatics Gateway exports the data, ensuring a seamless +and efficient data processing experience. + +#### Verification Strategy + +Configure suported inbound and export services with one or moreplug-ins and ensure the plug-ins are called in the automated testing. + +#### Target Release + +MONAI Deploy Informatics Gateway R4 + diff --git a/src/InformaticsGateway/Services/Export/ExportRequestDataMessage.cs b/src/Api/ExportRequestDataMessage.cs old mode 100644 new mode 100755 similarity index 72% rename from src/InformaticsGateway/Services/Export/ExportRequestDataMessage.cs rename to src/Api/ExportRequestDataMessage.cs index 7f319c9d5..e7dfec0fe --- a/src/InformaticsGateway/Services/Export/ExportRequestDataMessage.cs +++ b/src/Api/ExportRequestDataMessage.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * Copyright 2019-2021 NVIDIA Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,25 +17,42 @@ using System.Collections.Generic; using Ardalis.GuardClauses; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.Messaging.Events; -namespace Monai.Deploy.InformaticsGateway.Services.Export +namespace Monai.Deploy.InformaticsGateway.Api { public class ExportRequestDataMessage { private readonly ExportRequestEvent _exportRequest; - public byte[] FileContent { get; private set; } + public byte[] FileContent { get; private set; } = default!; public bool IsFailed { get; private set; } public IList Messages { get; init; } public FileExportStatus ExportStatus { get; private set; } public string Filename { get; } + /// + /// Optional list of data output plug-in type names to be executed by the . + /// + public List PlugInAssemblies + { + get + { + return _exportRequest.PluginAssemblies; + } + } + public string ExportTaskId { get { return _exportRequest.ExportTaskId; } } + public string WorkflowInstanceId + { + get { return _exportRequest.WorkflowInstanceId; } + } + public string CorrelationId { get { return _exportRequest.CorrelationId; } @@ -58,13 +75,13 @@ public ExportRequestDataMessage(ExportRequestEvent exportRequest, string filenam public void SetData(byte[] data) { - Guard.Against.Null(data); + Guard.Against.Null(data, nameof(data)); FileContent = data; } public void SetFailed(FileExportStatus fileExportStatus, string errorMessage) { - Guard.Against.NullOrWhiteSpace(errorMessage); + Guard.Against.NullOrWhiteSpace(errorMessage, nameof(errorMessage)); ExportStatus = fileExportStatus; IsFailed = true; diff --git a/src/Api/IsExternalinit.cs b/src/Api/IsExternalinit.cs index 2de6d1984..04a3f0728 100644 --- a/src/Api/IsExternalinit.cs +++ b/src/Api/IsExternalinit.cs @@ -16,7 +16,7 @@ using System.ComponentModel; -namespace System.Runtime.CompilerServices +namespace Monai.Deploy.InformaticsGateway.Api { /// /// Reserved to be used by the compiler for tracking metadata. diff --git a/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj b/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj index 60fcb53d4..1b6401d78 100644 --- a/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj +++ b/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj @@ -1,5 +1,5 @@ + + + + + Monai.Deploy.InformaticsGateway + Exe + net6.0 + Apache-2.0 + true + True + latest + ..\.sonarlint\project-monai_monai-deploy-informatics-gatewaycsharp.ruleset + true + be0fffc8-bebb-4509-a2c0-3c981e5415ab + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Parameter1>$(AssemblyName).Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj old mode 100644 new mode 100755 index 86c02d30c..f9e5fdaaa --- a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj +++ b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj @@ -1,5 +1,5 @@ + + + + + Monai.Deploy.InformaticsGateway.Test.PlugIns + Monai.Deploy.InformaticsGateway.Test.PlugIns + net6.0 + enable + enable + + + + + + + diff --git a/src/InformaticsGateway/Test/Plug-ins/TestInputDataPlugIns.cs b/src/InformaticsGateway/Test/Plug-ins/TestInputDataPlugIns.cs new file mode 100644 index 000000000..61640d386 --- /dev/null +++ b/src/InformaticsGateway/Test/Plug-ins/TestInputDataPlugIns.cs @@ -0,0 +1,83 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; + +namespace Monai.Deploy.InformaticsGateway.Test.PlugIns +{ + [PlugInName("TestInputDataPlugInAddWorkflow")] + public class TestInputDataPlugInAddWorkflow : IInputDataPlugIn + { + public static readonly string TestString = "TestInputDataPlugIn executed!"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecuteAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata) + { + fileMetadata.Workflows.Add(TestString); + return Task.FromResult((dicomFile, fileMetadata)); + } + } + + [PlugInName("TestInputDataPlugInResumeWorkflow")] + public class TestInputDataPlugInResumeWorkflow : IInputDataPlugIn + { + public static readonly string WorkflowInstanceId = "ee04a4ac-abb3-412b-b3a7-662c96380379"; + public static readonly string TaskId = "45b20f97-2b38-4b9a-baeb-d15f9d496851"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecuteAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata) + { + fileMetadata.WorkflowInstanceId = WorkflowInstanceId; + fileMetadata.TaskId = TaskId; + return Task.FromResult((dicomFile, fileMetadata)); + } + } + + [PlugInName("TestInputDataPlugInModifyDicomFile")] + public class TestInputDataPlugInModifyDicomFile : IInputDataPlugIn + { + public static readonly DicomTag ExpectedTag = DicomTag.PatientAddress; + public static readonly string ExpectedValue = "Added by TestInputDataPlugInModifyDicomFile"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecuteAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata) + { + dicomFile.Dataset.Add(ExpectedTag, ExpectedValue); + return Task.FromResult((dicomFile, fileMetadata)); + } + } + + [PlugInName("TestInputDataPlugInVirtualAE")] + public class TestInputDataPlugInVirtualAE : IInputDataPlugIn + { + public static readonly DicomTag ExpectedTag = DicomTag.PatientAddress; + public static readonly string ExpectedValue = "Added by TestInputDataPlugInVirtualAE"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecuteAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata) + { + dicomFile.Dataset.Add(ExpectedTag, ExpectedValue); + return Task.FromResult((dicomFile, fileMetadata)); + } + } +} diff --git a/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs b/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs new file mode 100644 index 000000000..171e7c3d6 --- /dev/null +++ b/src/InformaticsGateway/Test/Plug-ins/TestOutputDataPlugIns.cs @@ -0,0 +1,52 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; + +namespace Monai.Deploy.InformaticsGateway.Test.PlugIns +{ + [PlugInName("TestOutputDataPlugInAddMessage")] + public class TestOutputDataPlugInAddMessage : IOutputDataPlugIn + { + public static readonly string ExpectedValue = "Hello from TestOutputDataPlugInAddMessage"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> ExecuteAsync(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage) + { + exportRequestDataMessage.Messages.Add(ExpectedValue); + return Task.FromResult((dicomFile, exportRequestDataMessage)); + } + } + + [PlugInName("TestOutputDataPlugInModifyDicomFile")] + public class TestOutputDataPlugInModifyDicomFile : IOutputDataPlugIn + { + public static readonly DicomTag ExpectedTag = DicomTag.PatientAddress; + public static readonly string ExpectedValue = "Added by TestOutputDataPlugInModifyDicomFile"; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> ExecuteAsync(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage) + { + dicomFile.Dataset.Add(ExpectedTag, ExpectedValue); + return Task.FromResult((dicomFile, exportRequestDataMessage)); + } + } +} diff --git a/src/InformaticsGateway/Test/ProgramTest.cs b/src/InformaticsGateway/Test/ProgramTest.cs index 8ea0ac349..30399fb4f 100644 --- a/src/InformaticsGateway/Test/ProgramTest.cs +++ b/src/InformaticsGateway/Test/ProgramTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,24 +15,17 @@ */ using System; -using System.IO; -using System.Reflection; +using xRetry; using Xunit; namespace Monai.Deploy.InformaticsGateway.Test { public class ProgramTest { - private const string PlugInDirectoryName = "plug-ins"; - - [Fact(DisplayName = "Program - runs properly")] + [RetryFact(maxRetries: 10, delayBetweenRetriesMs: 500, DisplayName = "Program - runs properly")] public void Startup_RunsProperly() { var workingDirectory = Environment.CurrentDirectory; - var plugInDirectory = Path.Combine(workingDirectory, PlugInDirectoryName); - Directory.CreateDirectory(plugInDirectory); - var file = Assembly.GetExecutingAssembly().Location; - File.Copy(file, Path.Combine(plugInDirectory, Path.GetFileName(file)), true); var host = Program.CreateHostBuilder(System.Array.Empty()).Build(); Assert.NotNull(host); diff --git a/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineFactoryTest.cs b/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineFactoryTest.cs new file mode 100644 index 000000000..a92ea359f --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineFactoryTest.cs @@ -0,0 +1,70 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class InputDataPlugInEngineFactoryTest + { + private readonly Mock> _logger; + private readonly FileSystem _fileSystem; + + public InputDataPlugInEngineFactoryTest() + { + _logger = new Mock>(); + _fileSystem = new FileSystem(); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void RegisteredPlugIns_WhenCalled_ReturnsListOfPlugIns() + { + var factory = new InputDataPlugInEngineFactory(_fileSystem, _logger.Object); + + var result = factory.RegisteredPlugIns(); + + Assert.Collection(result, + p => VerifyPlugIn(p, typeof(TestInputDataPlugInAddWorkflow)), + p => VerifyPlugIn(p, typeof(TestInputDataPlugInResumeWorkflow)), + p => VerifyPlugIn(p, typeof(TestInputDataPlugInModifyDicomFile)), + p => VerifyPlugIn(p, typeof(TestInputDataPlugInVirtualAE))); + + _logger.VerifyLogging($"{typeof(IInputDataPlugIn).Name} data plug-in found {typeof(TestInputDataPlugInAddWorkflow).GetCustomAttribute()?.Name}: {typeof(TestInputDataPlugInAddWorkflow).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + _logger.VerifyLogging($"{typeof(IInputDataPlugIn).Name} data plug-in found {typeof(TestInputDataPlugInResumeWorkflow).GetCustomAttribute()?.Name}: {typeof(TestInputDataPlugInResumeWorkflow).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + _logger.VerifyLogging($"{typeof(IInputDataPlugIn).Name} data plug-in found {typeof(TestInputDataPlugInModifyDicomFile).GetCustomAttribute()?.Name}: {typeof(TestInputDataPlugInModifyDicomFile).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + _logger.VerifyLogging($"{typeof(IInputDataPlugIn).Name} data plug-in found {typeof(TestInputDataPlugInVirtualAE).GetCustomAttribute()?.Name}: {typeof(TestInputDataPlugInVirtualAE).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + } + + private void VerifyPlugIn(KeyValuePair values, Type type) + { + Assert.Equal(values.Key, type.GetCustomAttribute()?.Name); + Assert.Equal(values.Value, type.GetShortTypeAssemblyName()); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs b/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs new file mode 100644 index 000000000..0a7f647bc --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/InputDataPluginEngineTest.cs @@ -0,0 +1,155 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Monai.Deploy.Messaging.Events; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class InputDataPlugInEngineTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly Mock _serviceScope; + private readonly ServiceProvider _serviceProvider; + + public InputDataPlugInEngineTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _serviceScope = new Mock(); + + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void GivenAnInputDataPlugInEngine_WhenInitialized_ExpectParametersToBeValidated() + { + Assert.Throws(() => new InputDataPlugInEngine(null, null)); + Assert.Throws(() => new InputDataPlugInEngine(_serviceProvider, null)); + + _ = new InputDataPlugInEngine(_serviceProvider, _logger.Object); + } + + [Fact] + public void GivenAnInputDataPlugInEngine_WhenConfigureIsCalledWithBogusAssemblies_ThrowsException() + { + var pluginEngine = new InputDataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() { "SomeBogusAssemblye" }; + + var exceptions = Assert.Throws(() => pluginEngine.Configure(assemblies)); + + Assert.Single(exceptions.InnerExceptions); + Assert.True(exceptions.InnerException is PlugInLoadingException); + Assert.Contains("Error loading plug-in 'SomeBogusAssemblye'", exceptions.InnerException.Message); + } + + [Fact] + public void GivenAnInputDataPlugInEngine_WhenConfigureIsCalledWithAValidAssembly_ExpectNoExceptions() + { + var pluginEngine = new InputDataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() { typeof(TestInputDataPlugInAddWorkflow).AssemblyQualifiedName }; + + pluginEngine.Configure(assemblies); + } + + [Fact] + public async Task GivenAnInputDataPlugInEngine_WhenExecutePlugInsIsCalledWithoutConfigure_ThrowsException() + { + var pluginEngine = new InputDataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() { typeof(TestInputDataPlugInAddWorkflow).AssemblyQualifiedName }; + + var dicomFile = GenerateDicomFile(); + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID), + DataService.DicomWeb, + "calling", + "called"); + + await Assert.ThrowsAsync(async () => await pluginEngine.ExecutePlugInsAsync(dicomFile, dicomInfo)); + } + + [Fact] + public async Task GivenAnInputDataPlugInEngine_WhenExecutePlugInsIsCalled_ExpectDataIsProcessedByPlugInAsync() + { + var pluginEngine = new InputDataPlugInEngine(_serviceProvider, _logger.Object); + var assemblies = new List() + { + typeof(TestInputDataPlugInAddWorkflow).AssemblyQualifiedName, + typeof(TestInputDataPlugInModifyDicomFile).AssemblyQualifiedName, + typeof(TestInputDataPlugInResumeWorkflow).AssemblyQualifiedName, + }; + + pluginEngine.Configure(assemblies); + + var dicomFile = GenerateDicomFile(); + var dicomInfo = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SeriesInstanceUID), + dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID), + DataService.DicomWeb, + "calling", + "called"); + + var (resultDicomFile, resultDicomInfo) = await pluginEngine.ExecutePlugInsAsync(dicomFile, dicomInfo); + + Assert.Equal(resultDicomFile, dicomFile); + Assert.Equal(resultDicomInfo, dicomInfo); + Assert.True(dicomInfo.Workflows.Contains(TestInputDataPlugInAddWorkflow.TestString)); + Assert.Equal(TestInputDataPlugInModifyDicomFile.ExpectedValue, resultDicomFile.Dataset.GetString(TestInputDataPlugInModifyDicomFile.ExpectedTag)); + + Assert.Equal(resultDicomInfo.WorkflowInstanceId, TestInputDataPlugInResumeWorkflow.WorkflowInstanceId); + Assert.Equal(resultDicomInfo.TaskId, TestInputDataPlugInResumeWorkflow.TaskId); + } + + private static DicomFile GenerateDicomFile() + { + var dataset = new DicomDataset + { + { DicomTag.PatientID, "PID" }, + { DicomTag.StudyInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SeriesInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID } + }; + return new DicomFile(dataset); + } + } +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineFactoryTest.cs b/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineFactoryTest.cs new file mode 100644 index 000000000..d7a1fbad9 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineFactoryTest.cs @@ -0,0 +1,67 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class OutputDataPlugInEngineFactoryTest + { + private readonly Mock> _logger; + private readonly FileSystem _fileSystem; + + public OutputDataPlugInEngineFactoryTest() + { + _logger = new Mock>(); + _fileSystem = new FileSystem(); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void RegisteredPlugIns_WhenCalled_ReturnsListOfPlugIns() + { + var factory = new OutputDataPlugInEngineFactory(_fileSystem, _logger.Object); + + var result = factory.RegisteredPlugIns(); + + Assert.Collection(result, + p => VerifyPlugIn(p, typeof(TestOutputDataPlugInAddMessage)), + p => VerifyPlugIn(p, typeof(TestOutputDataPlugInModifyDicomFile)) + ); + + _logger.VerifyLogging($"{typeof(IOutputDataPlugIn).Name} data plug-in found {typeof(TestOutputDataPlugInAddMessage).GetCustomAttribute()?.Name}: {typeof(TestOutputDataPlugInAddMessage).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + _logger.VerifyLogging($"{typeof(IOutputDataPlugIn).Name} data plug-in found {typeof(TestOutputDataPlugInModifyDicomFile).GetCustomAttribute()?.Name}: {typeof(TestOutputDataPlugInModifyDicomFile).GetShortTypeAssemblyName()}.", LogLevel.Information, Times.Once()); + } + + private void VerifyPlugIn(KeyValuePair values, Type type) + { + Assert.Equal(values.Key, type.GetCustomAttribute()?.Name); + Assert.Equal(values.Value, type.GetShortTypeAssemblyName()); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs b/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs new file mode 100644 index 000000000..7a7445177 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Common/OutputDataPluginEngineTest.cs @@ -0,0 +1,152 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Common +{ + public class OutputDataPlugInEngineTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly Mock _serviceScope; + private readonly IDicomToolkit _dicomToolkit; + private readonly ServiceProvider _serviceProvider; + + public OutputDataPlugInEngineTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _serviceScope = new Mock(); + _dicomToolkit = new DicomToolkit(); + + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => _dicomToolkit); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void GivenAnOutputDataPlugInEngine_WhenInitialized_ExpectParametersToBeValidated() + { + Assert.Throws(() => new OutputDataPlugInEngine(null, null, null)); + Assert.Throws(() => new OutputDataPlugInEngine(_serviceProvider, null, null)); + Assert.Throws(() => new OutputDataPlugInEngine(_serviceProvider, _logger.Object, null)); + + _ = new OutputDataPlugInEngine(_serviceProvider, _logger.Object, _dicomToolkit); + } + + [Fact] + public void GivenAnOutputDataPlugInEngine_WhenConfigureIsCalledWithBogusAssemblies_ThrowsException() + { + var pluginEngine = new OutputDataPlugInEngine(_serviceProvider, _logger.Object, _dicomToolkit); + var assemblies = new List() { "SomeBogusAssemblye" }; + + var exceptions = Assert.Throws(() => pluginEngine.Configure(assemblies)); + + Assert.Single(exceptions.InnerExceptions); + Assert.True(exceptions.InnerException is PlugInLoadingException); + Assert.Contains("Error loading plug-in 'SomeBogusAssemblye'", exceptions.InnerException.Message); + } + + [Fact] + public void GivenAnOutputDataPlugInEngine_WhenConfigureIsCalledWithAValidAssembly_ExpectNoExceptions() + { + var pluginEngine = new OutputDataPlugInEngine(_serviceProvider, _logger.Object, _dicomToolkit); + var assemblies = new List() { typeof(TestOutputDataPlugInAddMessage).AssemblyQualifiedName }; + + pluginEngine.Configure(assemblies); + } + + [Fact] + public async Task GivenAnOutputDataPlugInEngine_WhenExecutePlugInsIsCalledWithoutConfigure_ThrowsException() + { + var pluginEngine = new OutputDataPlugInEngine(_serviceProvider, _logger.Object, _dicomToolkit); + var assemblies = new List() { typeof(TestOutputDataPlugInAddMessage).AssemblyQualifiedName }; + + var message = new ExportRequestDataMessage(new Messaging.Events.ExportRequestEvent + { + CorrelationId = Guid.NewGuid().ToString(), + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }, "filename.dcm"); + + await Assert.ThrowsAsync(async () => await pluginEngine.ExecutePlugInsAsync(message)); + } + + [Fact] + public async Task GivenAnOutputDataPlugInEngine_WhenExecutePlugInsIsCalled_ExpectDataIsProcessedByPlugInAsync() + { + var pluginEngine = new OutputDataPlugInEngine(_serviceProvider, _logger.Object, _dicomToolkit); + var assemblies = new List() + { + typeof(TestOutputDataPlugInAddMessage).AssemblyQualifiedName, + typeof(TestOutputDataPlugInModifyDicomFile).AssemblyQualifiedName + }; + + pluginEngine.Configure(assemblies); + + var dicomFile = GenerateDicomFile(); + var message = new ExportRequestDataMessage(new Messaging.Events.ExportRequestEvent + { + CorrelationId = Guid.NewGuid().ToString(), + MessageId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }, "filename.dcm"); + using var ms = new MemoryStream(); + await dicomFile.SaveAsync(ms); + message.SetData(ms.ToArray()); + + var resultMessage = await pluginEngine.ExecutePlugInsAsync(message); + using var resultMs = new MemoryStream(resultMessage.FileContent); + var resultDicomFile = await DicomFile.OpenAsync(resultMs); + + Assert.Equal(resultMessage, message); + Assert.True(resultMessage.Messages.Contains(TestOutputDataPlugInAddMessage.ExpectedValue)); + Assert.Equal(TestOutputDataPlugInModifyDicomFile.ExpectedValue, resultDicomFile.Dataset.GetString(TestOutputDataPlugInModifyDicomFile.ExpectedTag)); + } + + private static DicomFile GenerateDicomFile() + { + var dataset = new DicomDataset + { + { DicomTag.PatientID, "PID" }, + { DicomTag.StudyInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SeriesInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPInstanceUID, DicomUIDGenerator.GenerateDerivedFromUUID() }, + { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID } + }; + return new DicomFile(dataset); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs b/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs index 1e6694b15..1bad4f699 100644 --- a/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; using Moq; using Moq.Protected; using xRetry; @@ -191,21 +192,21 @@ public async Task GivenAInferenceRequestWithFromTheDatabaseWithPendingDownloads_ var restoredFile = new List { - new DicomFileStorageMetadata(Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString()), - new FhirFileStorageMetadata(Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString(), FhirStorageFormat.Json) + new DicomFileStorageMetadata(Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),Guid.NewGuid().ToString(),DataService.DicomWeb,"calling","called"), + new FhirFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), FhirStorageFormat.Json, DataService.FHIR, "origin"), }; _storageMetadataWrapperRepository.Setup(p => p.GetFileStorageMetdadataAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(restoredFile); + .ReturnsAsync(restoredFile); _inferenceRequestStore.SetupSequence(p => p.TakeAsync(It.IsAny())) - .Returns(Task.FromResult(request)) - .Returns(() => - { - cancellationTokenSource.Cancel(); - throw new OperationCanceledException("canceled"); - }); + .Returns(Task.FromResult(request)) + .Returns(() => + { + cancellationTokenSource.Cancel(); + throw new OperationCanceledException("canceled"); + }); - _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny())); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny())); var store = new DataRetrievalService(_logger.Object, _serviceScopeFactory.Object, _options); @@ -327,7 +328,7 @@ public async Task GivenAnInferenceRequest_WhenItCompletesRetrievalWithoutAnyFile req.RequestUri.ToString().StartsWith($"{url}studies/")), ItExpr.IsAny()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); _logger.VerifyLogging($"Error processing request: TransactionId = {request.TransactionId}.", LogLevel.Error, Times.Once()); } @@ -461,7 +462,7 @@ public async Task GivenAnInferenceRequestWithDicomUids_WhenProcessing_ExpectAllI ItExpr.IsAny()); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(4)); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Exactly(4)); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(4)); } [RetryFact(5, 250)] @@ -579,7 +580,7 @@ public async Task GivenAnInferenceRequestWithPatientId_WhenProcessing_ExpectAllI } _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(studyInstanceUids.Count)); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Exactly(studyInstanceUids.Count)); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(studyInstanceUids.Count)); } [RetryFact(5, 250)] @@ -697,7 +698,7 @@ public async Task GivenAnInferenceRequestWithAccessionNumber_WhenProcessing_Expe } _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(2)); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } [RetryFact(5, 250)] @@ -807,7 +808,7 @@ public async Task GivenAnInferenceRequestWithFhirResources_WhenProcessing_Expect req.RequestUri.PathAndQuery.Contains("Observation/2")), ItExpr.IsAny()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } private static HttpResponseMessage GenerateQueryResult(DicomTag dicomTag, string queryValue, List studyInstanceUids) @@ -843,4 +844,4 @@ private static void BlockUntilCancelled(CancellationToken token) WaitHandle.WaitAll(new[] { token.WaitHandle }); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Connectors/PayloadAssemblerTest.cs b/src/InformaticsGateway/Test/Services/Connectors/PayloadAssemblerTest.cs index 2de3a4b6f..078085558 100644 --- a/src/InformaticsGateway/Test/Services/Connectors/PayloadAssemblerTest.cs +++ b/src/InformaticsGateway/Test/Services/Connectors/PayloadAssemblerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; using Moq; using xRetry; using Xunit; @@ -67,19 +68,21 @@ public PayloadAssemblerTest() [Fact] public void GivenAPayloadAssembler_WhenInitialized_ExpectParametersToBeValidated() { - Assert.Throws(() => new PayloadAssembler(null, null, null)); - Assert.Throws(() => new PayloadAssembler(_options, null, null)); - Assert.Throws(() => new PayloadAssembler(_options, _logger.Object, null)); + Assert.Throws(() => new PayloadAssembler(null, null)); + Assert.Throws(() => new PayloadAssembler(_logger.Object, null)); } [RetryFact(10, 200)] public async Task GivenAFileStorageMetadata_WhenQueueingWihtoutSpecifyingATimeout_ExpectDefaultTimeoutToBeUsed() { - var payloadAssembler = new PayloadAssembler(_options, _logger.Object, _serviceScopeFactory.Object); + var payloadAssembler = new PayloadAssembler(_logger.Object, _serviceScopeFactory.Object); _ = Assert.ThrowsAsync(async () => await Task.Run(() => payloadAssembler.Dequeue(_cancellationTokenSource.Token))); - await payloadAssembler.Queue("A", new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt")); + await payloadAssembler.Queue( + "A", + new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt", new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }), + new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }); _logger.VerifyLogging($"Bucket A created with timeout {PayloadAssembler.DEFAULT_TIMEOUT}s.", LogLevel.Information, Times.Once()); payloadAssembler.Dispose(); @@ -91,7 +94,7 @@ public async Task GivenFileStorageMetadataInTheDatabase_AtServiceStartup_ExpectP { _repository.Setup(p => p.RemovePendingPayloadsAsync(It.IsAny())); - var payloadAssembler = new PayloadAssembler(_options, _logger.Object, _serviceScopeFactory.Object); + var payloadAssembler = new PayloadAssembler(_logger.Object, _serviceScopeFactory.Object); await Task.Delay(250); payloadAssembler.Dispose(); _cancellationTokenSource.Cancel(); @@ -102,11 +105,14 @@ public async Task GivenFileStorageMetadataInTheDatabase_AtServiceStartup_ExpectP [RetryFact(10, 200)] public async Task GivenAPayloadAssembler_WhenDisposed_ExpectResourceToBeCleanedUp() { - var payloadAssembler = new PayloadAssembler(_options, _logger.Object, _serviceScopeFactory.Object); + var payloadAssembler = new PayloadAssembler(_logger.Object, _serviceScopeFactory.Object); _ = Assert.ThrowsAsync(async () => await Task.Run(() => payloadAssembler.Dequeue(_cancellationTokenSource.Token))); - await payloadAssembler.Queue("A", new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt")); + await payloadAssembler.Queue( + "A", + new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt", new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }), + new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }); payloadAssembler.Dispose(); _cancellationTokenSource.Cancel(); @@ -118,13 +124,13 @@ public async Task GivenAPayloadAssembler_WhenDisposed_ExpectResourceToBeCleanedU [RetryFact(10, 200)] public async Task GivenAPayloadThatHasNotCompleteUploads_WhenProcessedByTimedEvent_ExpectToBeRemovedFromQueue() { - var payloadAssembler = new PayloadAssembler(_options, _logger.Object, _serviceScopeFactory.Object); + var payloadAssembler = new PayloadAssembler(_logger.Object, _serviceScopeFactory.Object); - var file1 = new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt"); - var file2 = new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt"); + var file1 = new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt", new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }); + var file2 = new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt", new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }); - await payloadAssembler.Queue("A", file1, 1); - await payloadAssembler.Queue("A", file2, 1); + await payloadAssembler.Queue("A", file1, new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 1); + await payloadAssembler.Queue("A", file2, new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 1); file1.SetFailed(); file2.SetUploaded(); @@ -138,11 +144,11 @@ public async Task GivenAPayloadThatHasNotCompleteUploads_WhenProcessedByTimedEve [RetryFact(10, 200)] public async Task GivenAPayloadThatHasCompletedUploads_WhenProcessedByTimedEvent_ExpectToBeAddedToQueue() { - var payloadAssembler = new PayloadAssembler(_options, _logger.Object, _serviceScopeFactory.Object); + var payloadAssembler = new PayloadAssembler(_logger.Object, _serviceScopeFactory.Object); - var file = new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt"); + var file = new TestStorageInfo(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "file1", ".txt", new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }); file.File.SetUploaded("bucket"); - await payloadAssembler.Queue("A", file, 1); + await payloadAssembler.Queue("A", file, new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 1); await Task.Delay(1001); var result = payloadAssembler.Dequeue(_cancellationTokenSource.Token); payloadAssembler.Dispose(); @@ -152,4 +158,4 @@ public async Task GivenAPayloadThatHasCompletedUploads_WhenProcessedByTimedEvent _logger.VerifyLoggingMessageBeginsWith($"Bucket A sent to processing queue with {result.Count} files", LogLevel.Information, Times.AtLeastOnce()); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Connectors/PayloadMoveActionHandlerTest.cs b/src/InformaticsGateway/Test/Services/Connectors/PayloadMoveActionHandlerTest.cs index 2bf054a68..2f63e787d 100644 --- a/src/InformaticsGateway/Test/Services/Connectors/PayloadMoveActionHandlerTest.cs +++ b/src/InformaticsGateway/Test/Services/Connectors/PayloadMoveActionHandlerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Connectors; +using Monai.Deploy.Messaging.Events; using Monai.Deploy.Storage.API; using Moq; using xRetry; @@ -94,13 +95,13 @@ public async Task GivenAPayloadInIncorrectState_WhenHandlerIsCalled_ExpectExcept }); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { State = Payload.PayloadState.Created, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), }, }; @@ -127,14 +128,14 @@ public async Task GivenAPayload_WhenHandlerFailedToCopyFiles_ExpectToBePutBackIn .ThrowsAsync(new Exception("error")); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { RetryCount = retryCount, State = Payload.PayloadState.Move, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), }, }; @@ -159,14 +160,14 @@ public async Task GivenAPayloadThatHasReachedMaximumRetries_WhenHandlerFailedToC .ThrowsAsync(new Exception("error")); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { RetryCount = 3, State = Payload.PayloadState.Move, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), }, }; @@ -190,12 +191,12 @@ public async Task GivenAPayload_WhenAllFilesAreMove_ExpectPayloadToBeAddedToNoti }); var correlationId = Guid.NewGuid(); - var file1 = new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + var file1 = new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"); file1.File.SetMoved("test"); file1.JsonFile.SetMoved("test"); - var file2 = new FhirFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Api.Rest.FhirStorageFormat.Json); + var file2 = new FhirFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Api.Rest.FhirStorageFormat.Json, Messaging.Events.DataService.FHIR, "origin"); file2.File.SetMoved("test"); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { RetryCount = 3, State = Payload.PayloadState.Move, @@ -206,7 +207,6 @@ public async Task GivenAPayload_WhenAllFilesAreMove_ExpectPayloadToBeAddedToNoti }, }; - var handler = new PayloadMoveActionHandler(_serviceScopeFactory.Object, _logger.Object, _options); await handler.MoveFilesAsync(payload, moveAction, notifyAction, _cancellationTokenSource.Token); @@ -217,4 +217,4 @@ public async Task GivenAPayload_WhenAllFilesAreMove_ExpectPayloadToBeAddedToNoti //_storageService.Verify(p => p.RemoveObjectAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.AtLeast(2)); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationActionHandlerTest.cs b/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationActionHandlerTest.cs index 0066a4fd1..6e6560206 100644 --- a/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationActionHandlerTest.cs +++ b/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationActionHandlerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.Messaging.API; +using Monai.Deploy.Messaging.Events; using Monai.Deploy.Messaging.Messages; using Moq; using Xunit; @@ -89,13 +90,13 @@ public async Task GivenAPayloadInIncorrectState_WhenHandlerIsCalled_ExpectExcept }); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { State = Payload.PayloadState.Created, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), }, }; @@ -120,14 +121,14 @@ public async Task GivenAPayload_WhenHandlerFailedToPublishNotification_ExpectToB .Throws(new Exception("error")); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { RetryCount = retryCount, State = Payload.PayloadState.Notify, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), }, }; @@ -150,14 +151,14 @@ public async Task GivenAPayloadThatHasReachedMaximumRetries_WhenHandlerFailedToP .Throws(new Exception("error")); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { RetryCount = 3, State = Payload.PayloadState.Notify, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), }, }; @@ -176,14 +177,14 @@ public async Task GivenAPayload_WhenMessageIsPublished_ExpectPayloadToBeDeleted( }); var correlationId = Guid.NewGuid(); - var payload = new Payload("key", correlationId.ToString(), 0) + var payload = new Payload("key", correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 0) { RetryCount = 3, State = Payload.PayloadState.Notify, Files = new List { - new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()), - new FhirFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Api.Rest.FhirStorageFormat.Json), + new DicomFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "calling", "called"), + new FhirFileStorageMetadata(correlationId.ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Api.Rest.FhirStorageFormat.Json, Messaging.Events.DataService.FHIR, "origin"), }, }; @@ -195,4 +196,4 @@ public async Task GivenAPayload_WhenMessageIsPublished_ExpectPayloadToBeDeleted( _repository.Verify(p => p.RemoveAsync(payload, _cancellationTokenSource.Token), Times.AtLeastOnce()); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationServiceTest.cs b/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationServiceTest.cs index 45f405bac..16063b9d9 100644 --- a/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Connectors/PayloadNotificationServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; @@ -30,7 +29,6 @@ using Monai.Deploy.InformaticsGateway.SharedTest; using Monai.Deploy.Messaging.API; using Monai.Deploy.Messaging.Events; -using Monai.Deploy.Messaging.Messages; using Moq; using xRetry; using Xunit; @@ -96,7 +94,7 @@ public void GivenAPayloadNotificationService_AtInitialization_ExpectParametersTo [RetryFact(10, 200)] public async Task GivenThePayloadNotificationService_WhenStopAsyncIsCalled_ExpectServiceToStopAnyProcessing() { - var payload = new Payload("test", Guid.NewGuid().ToString(), 100) { State = Payload.PayloadState.Move }; + var payload = new Payload("test", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 100) { State = Payload.PayloadState.Move }; _payloadAssembler.Setup(p => p.Dequeue(It.IsAny())) .Returns(() => { @@ -122,9 +120,9 @@ public void GivenPayloadsStoredInTheDatabase_WhenServiceStarts_ExpectThePayloads { var testData = new List { - new Payload("created-test", Guid.NewGuid().ToString(), 10){ State = Payload.PayloadState.Created}, - new Payload("upload-test", Guid.NewGuid().ToString(), 10){ State = Payload.PayloadState.Move}, - new Payload("notification-test", Guid.NewGuid().ToString(), 10) {State = Payload.PayloadState.Notify}, + new Payload("created-test", Guid.NewGuid().ToString(),Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 10){ State = Payload.PayloadState.Created}, + new Payload("upload-test", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" },10){ State = Payload.PayloadState.Move}, + new Payload("notification-test", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" },10) {State = Payload.PayloadState.Notify}, }; _payloadRepository.Setup(p => p.GetPayloadsInStateAsync(It.IsAny(), It.IsAny())) @@ -144,7 +142,7 @@ public void GivenPayloadsStoredInTheDatabase_WhenServiceStarts_ExpectThePayloads public void GivenAPayload_WhenDequedFromPayloadAssemblerAndFailedToBeProcessByTheMoveActionHandler() { var resetEvent = new ManualResetEventSlim(); - var payload = new Payload("test", Guid.NewGuid().ToString(), 100) { State = Payload.PayloadState.Move }; + var payload = new Payload("test", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 100) { State = Payload.PayloadState.Move }; _payloadAssembler.Setup(p => p.Dequeue(It.IsAny())) .Returns(payload); @@ -162,7 +160,7 @@ public void GivenAPayload_WhenDequedFromPayloadAssemblerAndFailedToBeProcessByTh [RetryFact(10, 200)] public void GivenAPayload_WhenDequedFromPayloadAssembler_ExpectThePayloadBeProcessedByTheMoveActionHandler() { - var payload = new Payload("test", Guid.NewGuid().ToString(), 100) { State = Payload.PayloadState.Move }; + var payload = new Payload("test", Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), new DataOrigin { DataService = Messaging.Events.DataService.DIMSE, Destination = "dest", Source = "source" }, 100) { State = Payload.PayloadState.Move }; _payloadAssembler.Setup(p => p.Dequeue(It.IsAny())).Returns(payload); _payloadRepository.Setup(p => p.GetPayloadsInStateAsync(It.IsAny(), It.IsAny())).ReturnsAsync(new List()); @@ -176,25 +174,5 @@ public void GivenAPayload_WhenDequedFromPayloadAssembler_ExpectThePayloadBeProce _logger.VerifyLogging($"Payload {payload.PayloadId} added to {service.ServiceName} for processing.", LogLevel.Information, Times.AtLeastOnce()); } - - private bool VerifyHelper(Payload payload, Message message) - { - var workflowRequestEvent = message.ConvertTo(); - if (workflowRequestEvent is null) return false; - if (workflowRequestEvent.Payload.Count != 1) return false; - if (workflowRequestEvent.PayloadId != payload.PayloadId) return false; - if (workflowRequestEvent.FileCount != payload.Files.Count) return false; - if (workflowRequestEvent.CorrelationId != payload.CorrelationId) return false; - if (workflowRequestEvent.Timestamp != payload.DateTimeCreated) return false; - if (workflowRequestEvent.CallingAeTitle != payload.Files.First().Source) return false; - if (workflowRequestEvent.CalledAeTitle != payload.Files.OfType().First().CalledAeTitle) return false; - - var workflowInPayload = payload.GetWorkflows(); - if (workflowRequestEvent.Workflows.Count() != workflowInPayload.Count) return false; - if (workflowRequestEvent.Workflows.Except(workflowInPayload).Any()) return false; - if (workflowInPayload.Except(workflowRequestEvent.Workflows).Any()) return false; - - return true; - } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/DicomWeb/StowServiceTest.cs b/src/InformaticsGateway/Test/Services/DicomWeb/StowServiceTest.cs index b414c87a2..1ab650619 100644 --- a/src/InformaticsGateway/Test/Services/DicomWeb/StowServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/DicomWeb/StowServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; using Monai.Deploy.InformaticsGateway.Services.DicomWeb; using Moq; using Xunit; @@ -47,8 +50,10 @@ public class StowServiceTest private readonly Mock> _loggerMultipartDicomInstanceReader; private readonly Mock> _loggerSingleDicomInstanceReader; private readonly Mock _serviceScope; + private readonly Mock _repository; private readonly Mock _streamsWriter; - private readonly MockFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; + private readonly IServiceProvider _serviceProvider; public StowServiceTest() { @@ -58,29 +63,23 @@ public StowServiceTest() _loggerMultipartDicomInstanceReader = new Mock>(); _loggerSingleDicomInstanceReader = new Mock>(); _serviceScope = new Mock(); + _repository = new Mock(); _streamsWriter = new Mock(); _fileSystem = new MockFileSystem(); - var serviceProvider = new Mock(); - serviceProvider - .Setup(x => x.GetService(typeof(ILogger))) - .Returns(_logger.Object); - serviceProvider - .Setup(x => x.GetService(typeof(ILogger))) - .Returns(_loggerMultipartDicomInstanceReader.Object); - serviceProvider - .Setup(x => x.GetService(typeof(ILogger))) - .Returns(_loggerSingleDicomInstanceReader.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStreamsWriter))) - .Returns(_streamsWriter.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IFileSystem))) - .Returns(_fileSystem); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => _loggerMultipartDicomInstanceReader.Object); + services.AddScoped(p => _loggerSingleDicomInstanceReader.Object); + services.AddScoped(p => _streamsWriter.Object); + services.AddScoped(p => _fileSystem); + services.AddScoped(p => _repository.Object); + + _serviceProvider = services.BuildServiceProvider(); _serviceFactory.Setup(p => p.CreateScope()) .Returns(_serviceScope.Object); - _serviceScope.SetupGet(p => p.ServiceProvider).Returns(serviceProvider.Object); + _serviceScope.SetupGet(p => p.ServiceProvider).Returns(_serviceProvider); } [Fact(DisplayName = "Constructor Test")] @@ -93,16 +92,35 @@ public void ConstructorTest() Assert.Null(exception); } + [Fact(DisplayName = "StoreAsync - Throws when virtual AE cannot be found")] + public async Task StoreAsync_ThrowsWhenVirtualAECannotBeFound() + { + var correlationId = Guid.NewGuid().ToString(); + var service = new StowService(_serviceFactory.Object, _configuration); + + var httpRequest = new Mock(); + + await Assert.ThrowsAsync(async () => + await service.StoreAsync(httpRequest.Object, "a.b.c.d", "aet", "workflow", correlationId, CancellationToken.None)); + } + [Fact(DisplayName = "StoreAsync - Throws with bad StudyInstanceUID")] public async Task StoreAsync_ThrowsWithInvalidStudyInstanceUid() { var correlationId = Guid.NewGuid().ToString(); var service = new StowService(_serviceFactory.Object, _configuration); + var vae = new VirtualApplicationEntity + { + Name = Guid.NewGuid().ToString(), + VirtualAeTitle = Guid.NewGuid().ToString(), + }; + + _repository.Setup(p => p.FindByAeTitleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(vae); var httpRequest = new Mock(); await Assert.ThrowsAsync(async () => - await service.StoreAsync(httpRequest.Object, "a.b.c.d", "workflow", correlationId, CancellationToken.None)); + await service.StoreAsync(httpRequest.Object, "a.b.c.d", "aet", "workflow", correlationId, CancellationToken.None)); } [Fact(DisplayName = "StoreAsync - Throws with bad content type header")] @@ -111,12 +129,19 @@ public async Task StoreAsync_ThrowsWithInvalidContentTypeHeader() var correlationId = Guid.NewGuid().ToString(); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; var service = new StowService(_serviceFactory.Object, _configuration); + var vae = new VirtualApplicationEntity + { + Name = Guid.NewGuid().ToString(), + VirtualAeTitle = Guid.NewGuid().ToString(), + }; + + _repository.Setup(p => p.FindByAeTitleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(vae); var httpRequest = new Mock(); httpRequest.SetupGet(p => p.Headers.ContentType).Returns(new StringValues("invalid-header")); await Assert.ThrowsAsync(async () => - await service.StoreAsync(httpRequest.Object, studyInstanceUid, "workflow", correlationId, CancellationToken.None)); + await service.StoreAsync(httpRequest.Object, studyInstanceUid, "aet", "workflow", correlationId, CancellationToken.None)); } [Fact(DisplayName = "StoreAsync - Throws with unsupported content type")] @@ -125,12 +150,19 @@ public async Task StoreAsync_ThrowsWithUnsupportedContentTypeHeader() var correlationId = Guid.NewGuid().ToString(); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; var service = new StowService(_serviceFactory.Object, _configuration); + var vae = new VirtualApplicationEntity + { + Name = Guid.NewGuid().ToString(), + VirtualAeTitle = Guid.NewGuid().ToString(), + }; + + _repository.Setup(p => p.FindByAeTitleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(vae); var httpRequest = new Mock(); httpRequest.SetupGet(p => p.ContentType).Returns(ContentTypes.ApplicationDicomJson); await Assert.ThrowsAsync(async () => - await service.StoreAsync(httpRequest.Object, studyInstanceUid, "workflow", correlationId, CancellationToken.None)); + await service.StoreAsync(httpRequest.Object, studyInstanceUid, "aet", "workflow", correlationId, CancellationToken.None)); } [Fact(DisplayName = "StoreAsync - handles single DICOM instance")] @@ -139,26 +171,59 @@ public async Task StoreAsync_HandlesSingleDicomInstance() var correlationId = Guid.NewGuid().ToString(); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; var service = new StowService(_serviceFactory.Object, _configuration); + var vae = new VirtualApplicationEntity + { + Name = Guid.NewGuid().ToString(), + VirtualAeTitle = Guid.NewGuid().ToString(), + }; - _streamsWriter.Setup(p => p.Save(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _streamsWriter.Setup(p => p.Save(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _repository.Setup(p => p.FindByAeTitleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(vae); var httpRequest = new Mock(); httpRequest.SetupGet(p => p.ContentType).Returns(ContentTypes.ApplicationDicom); httpRequest.SetupGet(p => p.HttpContext.Connection.RemoteIpAddress).Returns(IPAddress.Loopback); httpRequest.SetupGet(p => p.Body).Returns(Stream.Null); - var exception = await Record.ExceptionAsync(async () => await service.StoreAsync(httpRequest.Object, studyInstanceUid, "workflow", correlationId, CancellationToken.None)); + var exception = await Record.ExceptionAsync(async () => await service.StoreAsync(httpRequest.Object, studyInstanceUid, "aet", "workflow", correlationId, CancellationToken.None)); Assert.Null(exception); } [Fact(DisplayName = "StoreAsync - handles multiple DICOM instances")] public async Task StoreAsync_HandlesMultipleDicomInstances() + { + var correlationId = Guid.NewGuid().ToString(); + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var service = new StowService(_serviceFactory.Object, _configuration); + var vae = new VirtualApplicationEntity + { + Name = Guid.NewGuid().ToString(), + VirtualAeTitle = Guid.NewGuid().ToString(), + }; + + _streamsWriter.Setup(p => p.Save(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _repository.Setup(p => p.FindByAeTitleAsync(It.IsAny(), It.IsAny())).ReturnsAsync(vae); + + var httpRequest = new Mock(); + httpRequest.SetupGet(p => p.ContentType).Returns($"{ContentTypes.MultipartRelated}; boundary={Boundary}"); + httpRequest.SetupGet(p => p.HttpContext.Connection.RemoteIpAddress).Returns(IPAddress.Loopback); + + var body = await GenerateMultipartData(); + + httpRequest.SetupGet(p => p.Body).Returns(new MemoryStream(body)); + + var exception = await Record.ExceptionAsync(async () => await service.StoreAsync(httpRequest.Object, studyInstanceUid, "aet", "workflow", correlationId, CancellationToken.None)); + Assert.Null(exception); + } + + [Fact(DisplayName = "StoreAsync - handles multiple DICOM instances without virtual AE")] + public async Task GivenMultipleDicomInstances_WhenStoreAsyncIsCalledWithoutVirtualAE_SavesStreams() { var correlationId = Guid.NewGuid().ToString(); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; var service = new StowService(_serviceFactory.Object, _configuration); - _streamsWriter.Setup(p => p.Save(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + _streamsWriter.Setup(p => p.Save(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var httpRequest = new Mock(); httpRequest.SetupGet(p => p.ContentType).Returns($"{ContentTypes.MultipartRelated}; boundary={Boundary}"); @@ -168,8 +233,10 @@ public async Task StoreAsync_HandlesMultipleDicomInstances() httpRequest.SetupGet(p => p.Body).Returns(new MemoryStream(body)); - var exception = await Record.ExceptionAsync(async () => await service.StoreAsync(httpRequest.Object, studyInstanceUid, "workflow", correlationId, CancellationToken.None)); + var exception = await Record.ExceptionAsync(async () => await service.StoreAsync(httpRequest.Object, studyInstanceUid, null, "workflow", correlationId, CancellationToken.None)); Assert.Null(exception); + + _repository.Verify(p => p.FindByAeTitleAsync(It.IsAny(), It.IsAny()), Times.Never()); } private async Task GenerateMultipartData() diff --git a/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs b/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs index 7e9323d8a..73f4d1d3d 100644 --- a/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs +++ b/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,11 @@ using FellowOakDicom; using FellowOakDicom.Network; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -31,6 +34,7 @@ using Monai.Deploy.InformaticsGateway.Services.DicomWeb; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; using Moq; using Xunit; @@ -38,34 +42,53 @@ namespace Monai.Deploy.InformaticsGateway.Test.Services.DicomWeb { public class StreamsWriterTest { + private readonly Mock _serviceScopeFactory; private readonly Mock> _logger; private readonly MockFileSystem _fileSystem; private readonly Mock _uploadQueue; private readonly Mock _dicomToolkit; private readonly Mock _payloadAssembler; + private readonly Mock _serviceScope; + private readonly Mock _inputDataPlugInEngine; private readonly IOptions _configuration; + private readonly IServiceProvider _serviceProvider; public StreamsWriterTest() { + _serviceScopeFactory = new Mock(); + _serviceScope = new Mock(); _uploadQueue = new Mock(); _dicomToolkit = new Mock(); _payloadAssembler = new Mock(); _configuration = Options.Create(new InformaticsGatewayConfiguration()); _logger = new Mock>(); + _inputDataPlugInEngine = new Mock(); _fileSystem = new MockFileSystem(); _configuration.Value.Storage.LocalTemporaryStoragePath = "./temp"; + + var services = new ServiceCollection(); + services.AddScoped(p => _inputDataPlugInEngine.Object); + + _serviceProvider = services.BuildServiceProvider(); + + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.SetupGet(p => p.ServiceProvider).Returns(_serviceProvider); + + _inputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny())) + .Returns((DicomFile dicomFile, FileStorageMetadata fileMetadata) => Task.FromResult(new Tuple(dicomFile, fileMetadata))); } [Fact] public void GivenAStreamsWriter_WhenInitialized_ExpectParametersToBeValidated() { - Assert.Throws(() => new StreamsWriter(null, null, null, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, null, null, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, null, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, null)); - var exception = Record.Exception(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem)); + Assert.Throws(() => new StreamsWriter(null, null, null, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_serviceScopeFactory.Object, null, null, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, null, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, null, null, null)); + Assert.Throws(() => new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, null, null)); + Assert.Throws(() => new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, null)); + var exception = Record.Exception(() => new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem)); Assert.Null(exception); } @@ -77,12 +100,13 @@ public async Task GivenAHttpStream_WhenFailedToOpenAsDicomInstance_ExpectStatus4 .Throws(new Exception("error")); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); var streams = GenerateDicomStreams(studyInstanceUid); var result = await writer.Save( streams, studyInstanceUid, + null, "Workflow", Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); @@ -96,16 +120,17 @@ public async Task GivingADicomInstanceWithoutAMatchingStudyInstanceUid_WhenSavei var uids = new List(); SetupDicomToolkitMocks(uids); - _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny())); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); var correlationId = Guid.NewGuid().ToString(); var streams = GenerateDicomStreams(studyInstanceUid); var result = await writer.Save( streams, DicomUIDGenerator.GenerateDerivedFromUUID().UID, + null, "Workflow", correlationId, Guid.NewGuid().ToString()); @@ -126,24 +151,158 @@ public async Task GivingADicomInstanceWithoutAMatchingStudyInstanceUid_WhenSavei } _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); - _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task GivingAnEmptyRequest_WhenSaveingInstance_ExpectFailures() + { + var uids = new List(); + SetupDicomToolkitMocks(uids); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + + var correlationId = Guid.NewGuid().ToString(); + var streams = new List(); + var result = await writer.Save( + streams, + studyInstanceUid, + null, + "Workflow", + correlationId, + Guid.NewGuid().ToString()); + + Assert.Equal(StatusCodes.Status204NoContent, result.StatusCode); + + _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _inputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Never()); + _inputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task GivingAValidDicomInstanceWithZeroLength_WhenSaveingInstance_ExpectFailures() + { + var uids = new List(); + SetupDicomToolkitMocks(uids); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + + var correlationId = Guid.NewGuid().ToString(); + var streams = new List() { Stream.Null, Stream.Null }; + var result = await writer.Save( + streams, + studyInstanceUid, + null, + "Workflow", + correlationId, + Guid.NewGuid().ToString()); + + Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); + + _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _inputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Once()); + _inputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny()), Times.Never()); } [Fact] - public async Task GivingAValidDicomInstance_WhenSaveingInstance_ExpectInstanceToBeQueued() + public async Task GivingValidDicomInstances_WhenSaveingInstanceWithoutVirtualAE_ExpectInstanceToBeQueued() { var uids = new List(); SetupDicomToolkitMocks(uids); - _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny())); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + + var correlationId = Guid.NewGuid().ToString(); + var streams = GenerateDicomStreams(studyInstanceUid); + var result = await writer.Save( + streams, + studyInstanceUid, + null, + "Workflow", + correlationId, + Guid.NewGuid().ToString()); + + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + + var referencedSopSequence = result.Data.GetSequence(DicomTag.ReferencedSOPSequence); + Assert.Equal(streams.Count, referencedSopSequence.Items.Count); + + foreach (var item in referencedSopSequence.Items) + { + var uid = item.GetSingleValue(DicomTag.ReferencedSOPInstanceUID); + Assert.Contains(uids, p => p.SopInstanceUid == uid); + uid = item.GetSingleValue(DicomTag.ReferencedSOPClassUID); + Assert.Contains(uids, p => p.SopClassUid == uid); + } + + _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(streams.Count)); + _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(streams.Count)); + _inputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Once()); + _inputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny()), Times.Exactly(streams.Count)); + } + + [Fact] + public async Task GivingValidDicomInstances_WhenUnableToOpenInstance_ExpectZeroFailedSOPSequence() + { + var uids = new List(); + + _dicomToolkit.Setup(p => p.OpenAsync(It.IsAny(), It.IsAny())).Throws(new Exception("error")); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + + var correlationId = Guid.NewGuid().ToString(); + var streams = GenerateDicomStreams(studyInstanceUid); + var result = await writer.Save( + streams, + studyInstanceUid, + null, + "Workflow", + correlationId, + Guid.NewGuid().ToString()); + + Assert.Equal(StatusCodes.Status409Conflict, result.StatusCode); + + var failedSopSequence = result.Data.GetSequence(DicomTag.FailedSOPSequence); + Assert.Equal(0, failedSopSequence.Items.Count); + + _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _inputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Once()); + _inputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task GivingValidDicomInstances_WhenSavingInstanceWithVirtualAE_ExpectInstanceToBeQueued() + { + var vae = new VirtualApplicationEntity + { + Name = Guid.NewGuid().ToString(), + VirtualAeTitle = Guid.NewGuid().ToString(), + PlugInAssemblies = new List { "A", "B" }, + }; + var uids = new List(); + SetupDicomToolkitMocks(uids); + _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); + var writer = new StreamsWriter(_serviceScopeFactory.Object, _uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); var correlationId = Guid.NewGuid().ToString(); var streams = GenerateDicomStreams(studyInstanceUid); var result = await writer.Save( streams, studyInstanceUid, + vae, "Workflow", correlationId, Guid.NewGuid().ToString()); @@ -162,7 +321,9 @@ public async Task GivingAValidDicomInstance_WhenSaveingInstance_ExpectInstanceTo } _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(streams.Count)); - _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny()), Times.Exactly(streams.Count)); + _payloadAssembler.Verify(p => p.Queue(It.Is(p => p == correlationId), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(streams.Count)); + _inputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Once()); + _inputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny()), Times.Exactly(streams.Count)); } private void SetupDicomToolkitMocks(List uids) diff --git a/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs b/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs index 36b2a83f6..9c3e03738 100644 --- a/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Export/DicomWebExportServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -27,6 +28,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Rest; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; @@ -52,6 +55,7 @@ public class DicomWebExportServiceTest private readonly Mock _storageService; private readonly Mock _messageSubscriberService; private readonly Mock _messagePublisherService; + private readonly Mock _outputDataPlugInEngine; private readonly Mock _loggerFactory; private readonly Mock _httpClientFactory; private readonly Mock _inferenceRequestStore; @@ -69,6 +73,7 @@ public DicomWebExportServiceTest() _storageService = new Mock(); _messageSubscriberService = new Mock(); _messagePublisherService = new Mock(); + _outputDataPlugInEngine = new Mock(); _loggerFactory = new Mock(); _httpClientFactory = new Mock(); _inferenceRequestStore = new Mock(); @@ -81,28 +86,25 @@ public DicomWebExportServiceTest() _storageInfoProvider = new Mock(); _storageInfoProvider.Setup(p => p.HasSpaceAvailableForExport).Returns(true); - var serviceProvider = new Mock(); - serviceProvider - .Setup(x => x.GetService(typeof(IInferenceRequestRepository))) - .Returns(_inferenceRequestStore.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IMessageBrokerPublisherService))) - .Returns(_messagePublisherService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IMessageBrokerSubscriberService))) - .Returns(_messageSubscriberService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStorageService))) - .Returns(_storageService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStorageInfoProvider))) - .Returns(_storageInfoProvider.Object); + var services = new ServiceCollection(); + services.AddScoped(p => _inferenceRequestStore.Object); + services.AddScoped(p => _messagePublisherService.Object); + services.AddScoped(p => _messageSubscriberService.Object); + services.AddScoped(p => _outputDataPlugInEngine.Object); + services.AddScoped(p => _storageService.Object); + services.AddScoped(p => _storageInfoProvider.Object); + + var serviceProvider = services.BuildServiceProvider(); var scope = new Mock(); - scope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); + scope.Setup(x => x.ServiceProvider).Returns(serviceProvider); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(scope.Object); + _outputDataPlugInEngine.Setup(p => p.Configure(It.IsAny>())); + _outputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny())) + .Returns((ExportRequestDataMessage message) => Task.FromResult(message)); + _loggerFactory.Setup(p => p.CreateLogger(It.IsAny())).Returns(_loggerDicomWebClient.Object); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); } @@ -413,7 +415,7 @@ public async Task CompletesDataflow(HttpStatusCode httpStatusCode) private bool CheckMessage(Message message, ExportStatus exportStatus, FileExportStatus fileExportStatus) { - Guard.Against.Null(message); + Guard.Against.Null(message, nameof(message)); var exportEvent = message.ConvertTo(); return exportEvent.Status == exportStatus && diff --git a/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs b/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs index 4328fdab9..59b4923f3 100644 --- a/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs +++ b/src/InformaticsGateway/Test/Services/Export/ExportServiceBaseTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; @@ -22,6 +23,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Export; using Monai.Deploy.InformaticsGateway.Services.Storage; @@ -74,6 +77,7 @@ public class ExportServiceBaseTest private readonly Mock _storageService; private readonly Mock _messageSubscriberService; private readonly Mock _messagePublisherService; + private readonly Mock _outputDataPlugInEngine; private readonly Mock _logger; private readonly Mock _storageInfoProvider; private readonly IOptions _configuration; @@ -85,31 +89,31 @@ public ExportServiceBaseTest() _storageService = new Mock(); _messageSubscriberService = new Mock(); _messagePublisherService = new Mock(); + _outputDataPlugInEngine = new Mock(); _logger = new Mock(); _storageInfoProvider = new Mock(); _configuration = Options.Create(new InformaticsGatewayConfiguration()); _cancellationTokenSource = new CancellationTokenSource(); _serviceScopeFactory = new Mock(); - var serviceProvider = new Mock(); - serviceProvider - .Setup(x => x.GetService(typeof(IMessageBrokerPublisherService))) - .Returns(_messagePublisherService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IMessageBrokerSubscriberService))) - .Returns(_messageSubscriberService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStorageService))) - .Returns(_storageService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStorageInfoProvider))) - .Returns(_storageInfoProvider.Object); + var services = new ServiceCollection(); + services.AddScoped(p => _messagePublisherService.Object); + services.AddScoped(p => _messageSubscriberService.Object); + services.AddScoped(p => _outputDataPlugInEngine.Object); + services.AddScoped(p => _storageService.Object); + services.AddScoped(p => _storageInfoProvider.Object); + + var serviceProvider = services.BuildServiceProvider(); var scope = new Mock(); - scope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); + scope.Setup(x => x.ServiceProvider).Returns(serviceProvider); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(scope.Object); + _outputDataPlugInEngine.Setup(p => p.Configure(It.IsAny>())); + _outputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny())) + .Returns((ExportRequestDataMessage message) => Task.FromResult(message)); + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); _storageInfoProvider.Setup(p => p.HasSpaceAvailableForExport).Returns(true); } @@ -298,6 +302,8 @@ public async Task DataflowTest_EndToEnd() It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once()); + _outputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Exactly(5 * 2)); + _outputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny()), Times.Exactly(5 * 2)); } internal static MessageReceivedEventArgs CreateMessageReceivedEventArgs() diff --git a/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs b/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs index 920d3253d..3293a8434 100644 --- a/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Export/ScuExportServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -27,6 +28,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; @@ -50,6 +52,7 @@ public class ScuExportServiceTest private readonly Mock _storageService; private readonly Mock _messageSubscriberService; private readonly Mock _messagePublisherService; + private readonly Mock _outputDataPlugInEngine; private readonly Mock> _logger; private readonly Mock _scpLogger; private readonly Mock _serviceScopeFactory; @@ -69,6 +72,7 @@ public ScuExportServiceTest(DicomScpFixture dicomScp) _storageService = new Mock(); _messageSubscriberService = new Mock(); _messagePublisherService = new Mock(); + _outputDataPlugInEngine = new Mock(); _logger = new Mock>(); _scpLogger = new Mock(); _serviceScopeFactory = new Mock(); @@ -78,25 +82,18 @@ public ScuExportServiceTest(DicomScpFixture dicomScp) _repository = new Mock(); _storageInfoProvider = new Mock(); - var serviceProvider = new Mock(); - serviceProvider - .Setup(x => x.GetService(typeof(IDestinationApplicationEntityRepository))) - .Returns(_repository.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IMessageBrokerPublisherService))) - .Returns(_messagePublisherService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IMessageBrokerSubscriberService))) - .Returns(_messageSubscriberService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStorageService))) - .Returns(_storageService.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IStorageInfoProvider))) - .Returns(_storageInfoProvider.Object); + var services = new ServiceCollection(); + services.AddScoped(p => _repository.Object); + services.AddScoped(p => _messagePublisherService.Object); + services.AddScoped(p => _messageSubscriberService.Object); + services.AddScoped(p => _outputDataPlugInEngine.Object); + services.AddScoped(p => _storageService.Object); + services.AddScoped(p => _storageInfoProvider.Object); + + var serviceProvider = services.BuildServiceProvider(); var scope = new Mock(); - scope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); + scope.Setup(x => x.ServiceProvider).Returns(serviceProvider); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(scope.Object); DicomScpFixture.Logger = _scpLogger.Object; @@ -104,6 +101,10 @@ public ScuExportServiceTest(DicomScpFixture dicomScp) _configuration.Value.Export.Retries.DelaysMilliseconds = new[] { 1 }; _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); _storageInfoProvider.Setup(p => p.HasSpaceAvailableForExport).Returns(true); + + _outputDataPlugInEngine.Setup(p => p.Configure(It.IsAny>())); + _outputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny())) + .Returns((ExportRequestDataMessage message) => Task.FromResult(message)); } [RetryFact(5, 250, DisplayName = "Constructor - throws on null params")] @@ -511,7 +512,7 @@ public async Task ExportCompletes() private bool CheckMessage(Message message, ExportStatus exportStatus, FileExportStatus fileExportStatus) { - Guard.Against.Null(message); + Guard.Against.Null(message, nameof(message)); var exportEvent = message.ConvertTo(); return exportEvent.Status == exportStatus && diff --git a/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs b/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs index 42e10e3f6..7bb377917 100644 --- a/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs +++ b/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; +using System.Net; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -52,7 +53,7 @@ public FhirJsonReaderTest() [Fact] public async Task GetContentAsync_WhenCalled_EnsuresArgumentsAreValid() { - var request = new Mock(); + var request = CreateHttpRquestMock(); var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var reader = new FhirJsonReader(_logger.Object, _options, _fileSystem); @@ -67,7 +68,7 @@ public async Task GetContentAsync_WhenCalled_EnsuresArgumentsAreValid() [Fact] public async Task GetContentAsync_WhenCalledWithEmptyContent_ThrowsException() { - var request = new Mock(); + var request = CreateHttpRquestMock(); var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirJson); @@ -88,7 +89,7 @@ await Assert.ThrowsAnyAsync(async () => [Fact] public async Task GetContentAsync_WhenCalledWithNonXmlContent_ThrowsException() { - var request = new Mock(); + var request = CreateHttpRquestMock(); var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirJson); @@ -111,7 +112,8 @@ await Assert.ThrowsAnyAsync(async () => [InlineData("{\"resourceType\":\"Patient\",\"id\":\" \",\"name\":[{\"use\":\"official\",\"family\":\"Monai\",\"given\":[\"Deploy\"]}]}")] public async Task GetContentAsync_WhenCalledWithNoId_ReturnsOriginalWithId(string xml) { - var request = new Mock(); + var request = CreateHttpRquestMock(); + var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirJson); @@ -131,5 +133,16 @@ public async Task GetContentAsync_WhenCalledWithNoId_ReturnsOriginalWithId(strin Assert.NotNull(results.Metadata); Assert.Contains($"\"id\": \"{correlationId}\"", results.RawData); } + + private Mock CreateHttpRquestMock() + { + var request = new Mock(); + var context = new Mock(); + var connection = new Mock(); + request.Setup(p => p.HttpContext).Returns(context.Object); + context.Setup(p => p.Connection).Returns(connection.Object); + connection.Setup(p => p.RemoteIpAddress).Returns(new IPAddress(16885952)); + return request; + } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs b/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs index 243e0fcad..827e1605c 100644 --- a/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ using System; using System.IO; using System.IO.Abstractions; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -30,6 +31,7 @@ using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.Services.Fhir; using Monai.Deploy.InformaticsGateway.Services.Storage; +using Monai.Deploy.Messaging.Events; using Moq; using xRetry; using Xunit; @@ -67,6 +69,11 @@ public FhirServiceTest() _fileSystem = new Mock(); _httpRequest = new Mock(); + var context = new Mock(); + var connection = new Mock(); + _httpRequest.Setup(p => p.HttpContext).Returns(context.Object); + context.Setup(p => p.Connection).Returns(connection.Object); + connection.Setup(p => p.RemoteIpAddress).Returns(new IPAddress(16885952)); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); @@ -151,7 +158,7 @@ public async Task StoreAsync_WhenCalledWithValidContent_ShallQueueForProessing(s Assert.Equal(StatusCodes.Status201Created, results.StatusCode); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Once()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs b/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs index c01dd00cc..e0b6a27f2 100644 --- a/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs +++ b/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; +using System.Net; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -128,7 +129,7 @@ await Assert.ThrowsAsync(async () => [InlineData("")] public async Task GetContentAsync_WhenCalledWithNoId_ReturnsOriginalWithId(string xml) { - var request = new Mock(); + var request = CreateHttpRquestMock(); var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirXml); @@ -148,5 +149,16 @@ public async Task GetContentAsync_WhenCalledWithNoId_ReturnsOriginalWithId(strin Assert.NotNull(results.Metadata); Assert.Contains($"", results.RawData); } + + private Mock CreateHttpRquestMock() + { + var request = new Mock(); + var context = new Mock(); + var connection = new Mock(); + request.Setup(p => p.HttpContext).Returns(context.Object); + context.Setup(p => p.Connection).Returns(connection.Object); + connection.Setup(p => p.RemoteIpAddress).Returns(new IPAddress(16885952)); + return request; + } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs index 6671a082a..8114c7430 100644 --- a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ using Monai.Deploy.InformaticsGateway.Services.HealthLevel7; using Monai.Deploy.InformaticsGateway.Services.Storage; using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; using Moq; using xRetry; using Xunit; @@ -264,7 +265,7 @@ public async Task GivenATcpClientWithHl7Messages_WhenStorageSpaceIsLow_ExpectToD clientAdapter.Verify(p => p.Close(), Times.AtLeastOnce()); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [RetryFact(10, 250)] @@ -304,7 +305,7 @@ public async Task GivenATcpClientWithHl7Messages_WhenDisconnected_ExpectMessageT await Task.Delay(500).ConfigureAwait(false); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Exactly(3)); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny()), Times.Exactly(3)); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs index ebd2364ff..f57600a94 100644 --- a/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs +++ b/src/InformaticsGateway/Test/Services/Http/DestinationAeTitleControllerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,9 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Http; using Monai.Deploy.InformaticsGateway.Services.Scu; using Moq; @@ -45,6 +47,7 @@ public class DestinationAeTitleControllerTest private readonly Mock> _logger; private readonly Mock _scuQueue; private readonly Mock _repository; + private readonly Mock> _pluginFactory; public DestinationAeTitleControllerTest() { @@ -73,12 +76,14 @@ public DestinationAeTitleControllerTest() }); _repository = new Mock(); + _pluginFactory = new Mock>(); var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; _controller = new DestinationAeTitleController( _logger.Object, _repository.Object, - _scuQueue.Object) + _scuQueue.Object, + _pluginFactory.Object) { ProblemDetailsFactory = _problemDetailsFactory.Object, ControllerContext = controllerContext, @@ -420,7 +425,8 @@ public async Task Update_ReturnsUpdatedWhenUserIsAuthenticated() var controller = new DestinationAeTitleController( _logger.Object, _repository.Object, - _scuQueue.Object) + _scuQueue.Object, + _pluginFactory.Object) { ProblemDetailsFactory = _problemDetailsFactory.Object, ControllerContext = controllerContext, @@ -592,5 +598,41 @@ public async Task Delete_ShallReturnProblemOnFailure() } #endregion Delete + + #region GetPlugIns + + [RetryFact(5, 250, DisplayName = "GetPlugIns - Shall return registered plug-ins")] + public void GetPlugIns_ReturnsRegisteredPlugIns() + { + var input = new Dictionary() { { "A", "1" }, { "B", "3" }, { "C", "3" } }; + + _pluginFactory.Setup(p => p.RegisteredPlugIns()).Returns(input); + + var result = _controller.GetPlugIns(); + var okObjectResult = result.Result as OkObjectResult; + var response = okObjectResult.Value as IDictionary; + Assert.NotNull(response); + Assert.Equal(input, response); + + _pluginFactory.Verify(p => p.RegisteredPlugIns(), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "GetPlugIns - Shall return problem on failure")] + public void GetPlugIns_ShallReturnProblemOnFailure() + { + _pluginFactory.Setup(p => p.RegisteredPlugIns()).Throws(new Exception("error")); + + var result = _controller.GetPlugIns(); + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error reading data input plug-ins.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + _pluginFactory.Verify(p => p.RegisteredPlugIns(), Times.Once()); + } + + #endregion GetPlugIns } } diff --git a/src/InformaticsGateway/Test/Services/Http/DicomWeb/StowControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/DicomWeb/StowControllerTest.cs index 050af44bc..9bc24c9c4 100644 --- a/src/InformaticsGateway/Test/Services/Http/DicomWeb/StowControllerTest.cs +++ b/src/InformaticsGateway/Test/Services/Http/DicomWeb/StowControllerTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,7 +121,7 @@ public async Task StoreInstances_ReturnsProblemDetailWhenStorageSpaceIsLow(strin [InlineData("workflow")] public async Task StoreInstances_ReturnsProblemDetailException(string workflow) { - _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("error")); var result = await _controller.StoreInstances(workflow); @@ -142,7 +142,7 @@ public async Task StoreInstances_ReturnsProblemDetailException(string workflow) [InlineData("workflow")] public async Task StoreInstances_ReturnsOk(string workflow) { - _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new StowResult { StatusCode = StatusCodes.Status200OK, @@ -163,7 +163,7 @@ public async Task StoreInstances_ReturnsOk(string workflow) [InlineData("a", "workflow")] public async Task StoreInstancesToStudy_ReturnsProblemDetailWithInvalidUid(string studyInstanceUid, string workflow) { - _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new DicomValidationException("content", DicomVR.SL, "error")); var result = await _controller.StoreInstancesToStudy(studyInstanceUid, workflow); @@ -184,7 +184,7 @@ public async Task StoreInstancesToStudy_ReturnsProblemDetailWithInvalidUid(strin [InlineData("1.2.3.4.5", "workflow")] public async Task StoreInstancesToStudy_ReturnsProblemDetailException(string studyInstanceUid, string workflow) { - _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("error")); var result = await _controller.StoreInstancesToStudy(studyInstanceUid, workflow); @@ -205,7 +205,7 @@ public async Task StoreInstancesToStudy_ReturnsProblemDetailException(string stu [InlineData("1.2.3.4.5", "workflow")] public async Task StoreInstancesToStudy_ReturnsOk(string studyInstanceUid, string workflow) { - _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + _stowService.Setup(p => p.StoreAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new StowResult { StatusCode = StatusCodes.Status200OK, diff --git a/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs index 3f97efea1..5804056e5 100644 --- a/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs +++ b/src/InformaticsGateway/Test/Services/Http/MonaiAeTitleControllerTest.cs @@ -28,7 +28,9 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Common; using Monai.Deploy.InformaticsGateway.Services.Http; using Monai.Deploy.InformaticsGateway.Services.Scp; using Moq; @@ -45,6 +47,7 @@ public class MonaiAeTitleControllerTest private readonly Mock> _logger; private readonly Mock _aeChangedNotificationService; private readonly Mock _repository; + private readonly Mock> _pluginFactory; public MonaiAeTitleControllerTest() { @@ -73,12 +76,14 @@ public MonaiAeTitleControllerTest() }); _repository = new Mock(); + _pluginFactory = new Mock>(); var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() { User = new ClaimsPrincipal(new GenericIdentity(TestUsername)) } }; _controller = new MonaiAeTitleController( _logger.Object, _aeChangedNotificationService.Object, - _repository.Object) + _repository.Object, + _pluginFactory.Object) { ControllerContext = controllerContext, ProblemDetailsFactory = _problemDetailsFactory.Object @@ -307,6 +312,7 @@ public async Task Create_ShallReturnCreatedAtAction() #endregion Create #region Update + [RetryFact(DisplayName = "Update - Shall return updated")] public async Task Update_ReturnsUpdated() { @@ -319,6 +325,7 @@ public async Task Update_ReturnsUpdated() Workflows = new List { "1", "2", "3" }, AllowedSopClasses = new List { "1.2.3" }, IgnoredSopClasses = new List { "a.b.c" }, + PlugInAssemblies = new List { "A", "B" }, }; _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(entity)); @@ -367,6 +374,7 @@ public async Task Update_ReturnsUpdated_WithoutUpdatingAET() Workflows = new List { "1", "2" }, AllowedSopClasses = new List { "1.2" }, IgnoredSopClasses = new List { "a.b" }, + PlugInAssemblies = new List { "A", "B" }, }; _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(originalEntity)); @@ -385,6 +393,9 @@ public async Task Update_ReturnsUpdated_WithoutUpdatingAET() Assert.Equal(entity.Workflows, updatedEntity.Workflows); Assert.Equal(entity.AllowedSopClasses, updatedEntity.AllowedSopClasses); Assert.Equal(entity.IgnoredSopClasses, updatedEntity.IgnoredSopClasses); + Assert.Equal(entity.PlugInAssemblies, updatedEntity.PlugInAssemblies); + Assert.Collection(entity.PlugInAssemblies, p => p.Equals("A", StringComparison.CurrentCultureIgnoreCase), p => p.Equals("B", StringComparison.CurrentCultureIgnoreCase)); + Assert.NotNull(updatedEntity.DateTimeUpdated); Assert.Equal(TestUsername, updatedEntity.UpdatedBy); @@ -543,5 +554,41 @@ public async Task Delete_ShallReturnProblemOnFailure() } #endregion Delete + + #region GetPlugIns + + [RetryFact(5, 250, DisplayName = "GetPlugIns - Shall return registered plug-ins")] + public void GetPlugIns_ReturnsRegisteredPlugIns() + { + var input = new Dictionary() { { "A", "1" }, { "B", "3" }, { "C", "3" } }; + + _pluginFactory.Setup(p => p.RegisteredPlugIns()).Returns(input); + + var result = _controller.GetPlugIns(); + var okObjectResult = result.Result as OkObjectResult; + var response = okObjectResult.Value as IDictionary; + Assert.NotNull(response); + Assert.Equal(input, response); + + _pluginFactory.Verify(p => p.RegisteredPlugIns(), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "GetPlugIns - Shall return problem on failure")] + public void GetPlugIns_ShallReturnProblemOnFailure() + { + _pluginFactory.Setup(p => p.RegisteredPlugIns()).Throws(new Exception("error")); + + var result = _controller.GetPlugIns(); + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error reading data input plug-ins.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + _pluginFactory.Verify(p => p.RegisteredPlugIns(), Times.Once()); + } + + #endregion GetPlugIns } } diff --git a/src/InformaticsGateway/Test/Services/Http/VirtualAeTitleControllerTest.cs b/src/InformaticsGateway/Test/Services/Http/VirtualAeTitleControllerTest.cs new file mode 100644 index 000000000..fa7127b63 --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Http/VirtualAeTitleControllerTest.cs @@ -0,0 +1,527 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Database.Api.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Common; +using Monai.Deploy.InformaticsGateway.Services.Http; +using Moq; +using xRetry; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Http +{ + public class VirtualAeTitleControllerTest + { + private static readonly string TestUsername = "test-user"; + private readonly VirtualAeTitleController _controller; + private readonly Mock _problemDetailsFactory; + private readonly Mock> _logger; + private readonly Mock _repository; + private readonly Mock> _pluginFactory; + + public VirtualAeTitleControllerTest() + { + _logger = new Mock>(); + + _problemDetailsFactory = new Mock(); + _problemDetailsFactory.Setup(_ => _.CreateProblemDetails( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()) + ) + .Returns((HttpContext httpContext, int? statusCode, string title, string type, string detail, string instance) => + { + return new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance + }; + }); + + _repository = new Mock(); + _pluginFactory = new Mock>(); + + var controllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() { User = new ClaimsPrincipal(new GenericIdentity(TestUsername)) } }; + _controller = new VirtualAeTitleController( + _logger.Object, + _repository.Object, + _pluginFactory.Object) + { + ControllerContext = controllerContext, + ProblemDetailsFactory = _problemDetailsFactory.Object + }; + } + + #region Get + + [RetryFact(5, 250, DisplayName = "Get - Shall return available MONAI AETs")] + public async Task Get_ShallReturnAllMonaiAets() + { + var data = new List(); + for (var i = 1; i <= 5; i++) + { + data.Add(new VirtualApplicationEntity() + { + VirtualAeTitle = $"AET{i}", + Name = $"AET{i}", + Workflows = new List { "A", "B" } + }); + } + + _repository.Setup(p => p.ToListAsync(It.IsAny())).Returns(Task.FromResult(data)); + + var result = await _controller.Get(); + var okObjectResult = result.Result as OkObjectResult; + var response = okObjectResult.Value as IEnumerable; + Assert.Equal(data.Count, response.Count()); + _repository.Verify(p => p.ToListAsync(It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Get - Shall return problem on failure")] + public async Task Get_ShallReturnProblemOnFailure() + { + _repository.Setup(p => p.ToListAsync(It.IsAny())).Throws(new Exception("error")); + + var result = await _controller.Get(); + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error querying database.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + } + + #endregion Get + + #region GetAeTitle + + [RetryFact(5, 250, DisplayName = "GetAeTitle - Shall return matching object")] + public async Task GetAeTitle_ReturnsAMatch() + { + var value = "AET"; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns( + Task.FromResult( + new VirtualApplicationEntity + { + VirtualAeTitle = value, + Name = value, + Workflows = new List { "A", "B" } + })); + + var result = await _controller.GetAeTitle(value); + var okObjectResult = result.Result as OkObjectResult; + var response = okObjectResult.Value as VirtualApplicationEntity; + Assert.NotNull(response); + Assert.Equal(value, response.VirtualAeTitle); + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "GetAeTitle - Shall return 404 if not found")] + public async Task GetAeTitle_Returns404IfNotFound() + { + var value = "AET"; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(default(VirtualApplicationEntity))); + + var result = await _controller.GetAeTitle(value); + + Assert.IsType(result.Result); + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "GetAeTitle - Shall return problem on failure")] + public async Task GetAeTitle_ShallReturnProblemOnFailure() + { + var value = "AET"; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Throws(new Exception("error")); + + var result = await _controller.GetAeTitle(value); + + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error querying Virtual Application Entity.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + } + + #endregion GetAeTitle + + #region Create + + [RetryFact(DisplayName = "Create - Shall return Conflict if entity already exists")] + public async Task Create_ShallReturnConflictIfIEntityAlreadyExists() + { + _repository.Setup(p => p.ContainsAsync(It.IsAny>>(), It.IsAny())).ReturnsAsync(true); + + var virtualAeTitle = new VirtualApplicationEntity + { + Name = "AET1", + VirtualAeTitle = "AET1", + Workflows = new List { "A", "B" } + }; + + var result = await _controller.Create(virtualAeTitle); + + Assert.NotNull(result); + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("AE Title already exists", problem.Title); + Assert.Equal("A Virtual Application Entity with the same name 'AET1' already exists.", problem.Detail); + Assert.Equal((int)HttpStatusCode.Conflict, problem.Status); + } + + [Fact(DisplayName = "Create - Shall return BadRequest when validation fails")] + public async Task Create_ShallReturnBadRequestOnValidationFailure() + { + var virtualAeTitle = new VirtualApplicationEntity + { + Name = "AeTitleIsTooooooLooooong", + VirtualAeTitle = "AeTitleIsTooooooLooooong", + Workflows = new List { "A", "B" } + }; + + var result = await _controller.Create(virtualAeTitle); + + Assert.NotNull(result); + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Validation error.", problem.Title); + Assert.Equal("'AeTitleIsTooooooLooooong' is not a valid AE Title (source: virtualAeTitle).", problem.Detail); + Assert.Equal((int)HttpStatusCode.BadRequest, problem.Status); + } + + [RetryFact(5, 250, DisplayName = "Create - Shall return problem if failed to add")] + public async Task Create_ShallReturnBadRequestOnAddFailure() + { + var aeTitle = "AET"; + var virtualAeTitle = new VirtualApplicationEntity + { + Name = aeTitle, + VirtualAeTitle = aeTitle, + Workflows = new List { "A", "B" } + }; + + _repository.Setup(p => p.AddAsync(It.IsAny(), It.IsAny())).Throws(new Exception("error")); + + var result = await _controller.Create(virtualAeTitle); + + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error adding new Virtual Application Entity.", problem.Title); + Assert.Equal($"error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + + _repository.Verify(p => p.AddAsync(It.IsAny(), It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Create - Shall return CreatedAtAction")] + public async Task Create_ShallReturnCreatedAtAction() + { + var aeTitle = "AET"; + var virtualAeTitle = new VirtualApplicationEntity + { + Name = aeTitle, + VirtualAeTitle = aeTitle, + Workflows = new List { "A", "B" } + }; + + _repository.Setup(p => p.AddAsync(It.IsAny(), It.IsAny())); + + var result = await _controller.Create(virtualAeTitle); + + Assert.IsType(result.Result); + + _repository.Verify(p => p.AddAsync(It.Is(p => p.CreatedBy == TestUsername), It.IsAny()), Times.Once()); + } + + #endregion Create + + #region Update + + [RetryFact(DisplayName = "Update - Shall return updated")] + public async Task Update_ReturnsUpdated() + { + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = "AET", + Name = "AET", + Workflows = new List { "1", "2", "3" }, + PlugInAssemblies = new List { "A", "B", "C" }, + }; + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(entity)); + _repository.Setup(p => p.UpdateAsync(It.IsAny(), It.IsAny())); + + var result = await _controller.Edit(entity); + var okResult = result.Result as OkObjectResult; + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + var updatedEntity = okResult.Value as VirtualApplicationEntity; + + Assert.Equal(entity.VirtualAeTitle, updatedEntity.VirtualAeTitle); + Assert.Equal(entity.Name, updatedEntity.Name); + Assert.Equal(entity.Workflows, updatedEntity.Workflows); + Assert.Equal(entity.PlugInAssemblies, updatedEntity.PlugInAssemblies); + Assert.NotNull(updatedEntity.DateTimeUpdated); + Assert.Equal(TestUsername, updatedEntity.UpdatedBy); + + _repository.Verify(p => p.FindByNameAsync(entity.Name, It.IsAny()), Times.Once()); + _repository.Verify(p => p.UpdateAsync(entity, It.IsAny()), Times.Once()); + } + + [RetryFact(DisplayName = "Update - Shall return updated without updating AE Title")] + public async Task Update_ReturnsUpdated_WithoutUpdatingAET() + { + var originalEntity = new VirtualApplicationEntity + { + VirtualAeTitle = "AET", + Name = "AET", + Workflows = new List { "1", "2", "3" }, + }; + + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = "SHOUD-NOT-CHANGE", + Name = "AET", + Workflows = new List { "1", "2" }, + PlugInAssemblies = new List { "A", "B" }, + }; + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(originalEntity)); + _repository.Setup(p => p.UpdateAsync(It.IsAny(), It.IsAny())); + + var result = await _controller.Edit(entity); + var okResult = result.Result as OkObjectResult; + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + var updatedEntity = okResult.Value as VirtualApplicationEntity; + + Assert.NotEqual(entity.VirtualAeTitle, updatedEntity.VirtualAeTitle); + Assert.Equal(originalEntity.VirtualAeTitle, updatedEntity.VirtualAeTitle); + Assert.Equal(entity.Name, updatedEntity.Name); + Assert.Equal(entity.Workflows, updatedEntity.Workflows); + Assert.Equal(entity.PlugInAssemblies, updatedEntity.PlugInAssemblies); + Assert.Collection(entity.PlugInAssemblies, p => p.Equals("A", StringComparison.CurrentCultureIgnoreCase), p => p.Equals("B", StringComparison.CurrentCultureIgnoreCase)); + Assert.NotNull(updatedEntity.DateTimeUpdated); + Assert.Equal(TestUsername, updatedEntity.UpdatedBy); + + _repository.Verify(p => p.FindByNameAsync(entity.Name, It.IsAny()), Times.Once()); + _repository.Verify(p => p.UpdateAsync(updatedEntity, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Update - Shall return 404 if input is null")] + public async Task Update_Returns404IfInputIsNull() + { + var result = await _controller.Edit(null); + + Assert.IsType(result.Result); + } + + [RetryFact(5, 250, DisplayName = "Update - Shall return 404 if not found")] + public async Task Update_Returns404IfNotFound() + { + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = "AET", + Name = "AET", + Workflows = new List { "1", "2", "3" }, + }; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(default(VirtualApplicationEntity))); + + var result = await _controller.Edit(entity); + + Assert.IsType(result.Result); + _repository.Verify(p => p.FindByNameAsync(entity.Name, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Update - Shall return problem on failure")] + public async Task Update_ShallReturnProblemOnFailure() + { + var value = "AET"; + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = value, + Name = "AET", + Workflows = new List { "1", "2", "3" }, + }; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(entity)); + _repository.Setup(p => p.UpdateAsync(It.IsAny(), It.IsAny())).Throws(new Exception("error")); + + var result = await _controller.Edit(entity); + + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error updating Virtual Application Entity.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Update - Shall return problem on validation failure")] + public async Task Update_ShallReturnBadRequestWithBadAeTitle() + { + var aeTitle = "TOOOOOOOOOOOOOOOOOOOOOOOLONG"; + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = aeTitle, + Name = "AET", + Workflows = new List { "1", "2", "3" }, + }; + + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(entity)); + var result = await _controller.Edit(entity); + + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Validation error.", problem.Title); + Assert.Equal($"'{aeTitle}' is not a valid AE Title (source: virtualAeTitle).", problem.Detail); + Assert.Equal((int)HttpStatusCode.BadRequest, problem.Status); + } + + #endregion Update + + #region Delete + + [RetryFact(5, 250, DisplayName = "Delete - Shall return deleted object")] + public async Task Delete_ReturnsDeleted() + { + var value = "AET"; + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = value, + Name = value + }; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(entity)); + + _repository.Setup(p => p.RemoveAsync(It.IsAny(), It.IsAny())); + + var result = await _controller.Delete(value); + var okObjectResult = result.Result as OkObjectResult; + var response = okObjectResult.Value as VirtualApplicationEntity; + Assert.NotNull(response); + Assert.Equal(value, response.VirtualAeTitle); + + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + _repository.Verify(p => p.RemoveAsync(entity, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Delete - Shall return 404 if not found")] + public async Task Delete_Returns404IfNotFound() + { + var value = "AET"; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(default(VirtualApplicationEntity))); + + var result = await _controller.Delete(value); + + Assert.IsType(result.Result); + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "Delete - Shall return problem on failure")] + public async Task Delete_ShallReturnProblemOnFailure() + { + var value = "AET"; + var entity = new VirtualApplicationEntity + { + VirtualAeTitle = value, + Name = value + }; + _repository.Setup(p => p.FindByNameAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(entity)); + _repository.Setup(p => p.RemoveAsync(It.IsAny(), It.IsAny())).Throws(new Exception("error")); + + var result = await _controller.Delete(value); + + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error deleting Virtual Application Entity.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + _repository.Verify(p => p.FindByNameAsync(value, It.IsAny()), Times.Once()); + } + + #endregion Delete + + #region GetPlugIns + + [RetryFact(5, 250, DisplayName = "GetPlugIns - Shall return registered plug-ins")] + public void GetPlugIns_ReturnsRegisteredPlugIns() + { + var input = new Dictionary() { { "A", "1" }, { "B", "3" }, { "C", "3" } }; + + _pluginFactory.Setup(p => p.RegisteredPlugIns()).Returns(input); + + var result = _controller.GetPlugIns(); + var okObjectResult = result.Result as OkObjectResult; + var response = okObjectResult.Value as IDictionary; + Assert.NotNull(response); + Assert.Equal(input, response); + + _pluginFactory.Verify(p => p.RegisteredPlugIns(), Times.Once()); + } + + [RetryFact(5, 250, DisplayName = "GetPlugIns - Shall return problem on failure")] + public void GetPlugIns_ShallReturnProblemOnFailure() + { + _pluginFactory.Setup(p => p.RegisteredPlugIns()).Throws(new Exception("error")); + + var result = _controller.GetPlugIns(); + var objectResult = result.Result as ObjectResult; + Assert.NotNull(objectResult); + var problem = objectResult.Value as ProblemDetails; + Assert.NotNull(problem); + Assert.Equal("Error reading data input plug-ins.", problem.Title); + Assert.Equal("error", problem.Detail); + Assert.Equal((int)HttpStatusCode.InternalServerError, problem.Status); + _pluginFactory.Verify(p => p.RegisteredPlugIns(), Times.Once()); + } + + #endregion GetPlugIns + } +} diff --git a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs index 96e465187..a5f4dd9f6 100644 --- a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs @@ -25,12 +25,15 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.Services.Scp; using Monai.Deploy.InformaticsGateway.Services.Storage; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; +using Monai.Deploy.Messaging.Events; using Moq; using xRetry; using Xunit; @@ -43,6 +46,7 @@ public class ApplicationEntityHandlerTest private readonly Mock _serviceScopeFactory; private readonly Mock _serviceScope; + private readonly Mock _inputDataPlugInEngine; private readonly Mock _payloadAssembler; private readonly Mock _uploadQueue; private readonly IOptions _options; @@ -54,6 +58,7 @@ public ApplicationEntityHandlerTest() _logger = new Mock>(); _serviceScopeFactory = new Mock(); _serviceScope = new Mock(); + _inputDataPlugInEngine = new Mock(); _payloadAssembler = new Mock(); _uploadQueue = new Mock(); @@ -64,6 +69,10 @@ public ApplicationEntityHandlerTest() services.AddScoped(p => _payloadAssembler.Object); services.AddScoped(p => _uploadQueue.Object); services.AddScoped(p => _fileSystem); + services.AddScoped(p => _inputDataPlugInEngine.Object); + + _inputDataPlugInEngine.Setup(p => p.Configure(It.IsAny>())); + _serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); @@ -123,7 +132,7 @@ public async Task GivenACStoreRequest_WhenTheSopClassIsInTheIgnoreList_ExpectIns await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [RetryFact(5, 250)] @@ -147,7 +156,7 @@ public async Task GivenACStoreRequest_WhenTheSopClassIsNotInTheAllowedList_Expec await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Never()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [RetryFact(5, 250)] @@ -157,7 +166,8 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic { AeTitle = "TESTAET", Name = "TESTAET", - Workflows = new List() { "AppA", "AppB", Guid.NewGuid().ToString() } + Workflows = new List() { "AppA", "AppB", Guid.NewGuid().ToString() }, + PlugInAssemblies = new List() { typeof(TestInputDataPlugInAddWorkflow).AssemblyQualifiedName } }; var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); @@ -166,11 +176,15 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic var request = GenerateRequest(); var dicomToolkit = new DicomToolkit(); var uids = dicomToolkit.GetStudySeriesSopInstanceUids(request.File); + _inputDataPlugInEngine.Setup(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny())) + .Returns((DicomFile dicomFile, FileStorageMetadata fileMetadata) => Task.FromResult(new Tuple(dicomFile, fileMetadata))); await handler.HandleInstanceAsync(request, aet.AeTitle, "CALLING", Guid.NewGuid(), uids); _uploadQueue.Verify(p => p.Queue(It.IsAny()), Times.Once()); - _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _payloadAssembler.Verify(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + _inputDataPlugInEngine.Verify(p => p.Configure(It.IsAny>()), Times.Once()); + _inputDataPlugInEngine.Verify(p => p.ExecutePlugInsAsync(It.IsAny(), It.IsAny()), Times.Once()); } [RetryFact(5, 250)] @@ -214,4 +228,4 @@ private static DicomCStoreRequest GenerateRequest() return new DicomCStoreRequest(file); } } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs b/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs index 1c3dcde04..d4d8bace5 100644 --- a/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/ScpServiceTest.cs @@ -44,7 +44,6 @@ public class ScpServiceTest private readonly Mock _associationDataProvider; private readonly Mock _loggerFactory; private readonly Mock> _logger; - private readonly Mock _loggerInternal; private readonly Mock _appLifetime; private readonly IOptions _configuration; private readonly CancellationTokenSource _cancellationTokenSource; @@ -56,7 +55,6 @@ public ScpServiceTest() _associationDataProvider = new Mock(); _loggerFactory = new Mock(); _logger = new Mock>(); - _loggerInternal = new Mock(); _appLifetime = new Mock(); _configuration = Options.Create(new InformaticsGatewayConfiguration()); _cancellationTokenSource = new CancellationTokenSource(); @@ -67,10 +65,8 @@ public ScpServiceTest() _serviceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); _loggerFactory.Setup(p => p.CreateLogger(It.IsAny())).Returns(_logger.Object); - _associationDataProvider.Setup(p => p.GetLogger(It.IsAny())).Returns(_loggerInternal.Object); _associationDataProvider.Setup(p => p.Configuration).Returns(_configuration); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); - _loggerInternal.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); } [RetryFact(5, 250, DisplayName = "StartAsync - shall stop application if failed to start SCP listner")] @@ -122,7 +118,7 @@ public async Task CEcho_ShallRejectCEchoRequests() Assert.Equal(DicomRejectSource.ServiceUser, exception.RejectSource); Assert.Equal(DicomRejectResult.Permanent, exception.RejectResult); - _loggerInternal.VerifyLogging($"Verification service is disabled: rejecting association.", LogLevel.Warning, Times.Once()); + _logger.VerifyLogging($"Verification service is disabled: rejecting association.", LogLevel.Warning, Times.Once()); Assert.True(countdownEvent.Wait(1000)); } @@ -381,7 +377,7 @@ public async Task CStore_OnClientAbort() await client.SendAsync(_cancellationTokenSource.Token, DicomClientCancellationMode.ImmediatelyAbortAssociation); Assert.True(countdownEvent.Wait(2000)); - _loggerInternal.VerifyLogging($"Aborted {DicomAbortSource.ServiceUser} with reason {DicomAbortReason.NotSpecified}.", LogLevel.Warning, Times.Once()); + _logger.VerifyLogging($"Aborted {DicomAbortSource.ServiceUser} with reason {DicomAbortReason.NotSpecified}.", LogLevel.Warning, Times.Once()); } private ScpService CreateService() diff --git a/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs b/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs index b601a5876..26df5786a 100644 --- a/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ using Monai.Deploy.InformaticsGateway.Common; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Storage; +using Monai.Deploy.InformaticsGateway.SharedTest; using Monai.Deploy.Storage.API; using Moq; using xRetry; @@ -119,6 +120,34 @@ public async Task GivenADicomFileStorageMetadata_WhenQueuedForUpload_ExpectTwoFi _storageService.Verify(p => p.PutObjectAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()), Times.Exactly(2)); } + [RetryFact(10, 250)] + public async Task GivenADicomFileStorageMetadata_WhenVerificationFailsOver3Times_ExpectExceptionToBeThrow() + { + _options.Value.Storage.Retries.DelaysMilliseconds = new[] { 1 }; + var countdownEvent = new CountdownEvent(3); + _storageService.Setup(p => p.PutObjectAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())); + _storageService.Setup(p => p.VerifyObjectExistsAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(() => + { + countdownEvent.Signal(); + }) + .ReturnsAsync(false); + + var svc = new ObjectUploadService(_serviceScopeFactory.Object, _logger.Object, _options); + _ = svc.StartAsync(_cancellationTokenSource.Token); + + Assert.Equal(ServiceStatus.Running, svc.Status); + + var file = await GenerateDicomFileStorageMetadata(); + _uploadQueue.Queue(file); + + Assert.True(countdownEvent.Wait(TimeSpan.FromSeconds(3))); + + _storageService.Verify(p => p.PutObjectAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once()); + _storageService.Verify(p => p.VerifyObjectExistsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + _logger.VerifyLoggingMessageBeginsWith("Failed to upload file", LogLevel.Warning, Times.Once()); + } + [RetryFact(10, 25000)] public async Task GivenAFhirFileStorageMetadata_WhenQueuedForUpload_ExpectSingleFileToBeUploaded() { @@ -143,7 +172,7 @@ public async Task GivenAFhirFileStorageMetadata_WhenQueuedForUpload_ExpectSingle private async Task GenerateFhirFileStorageMetadata() { - var file = new FhirFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), FhirStorageFormat.Json); + var file = new FhirFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), FhirStorageFormat.Json, Messaging.Events.DataService.FHIR, "origin"); await file.SetDataStream("[]", TemporaryDataStorageLocation.Memory); file.PayloadId = Guid.NewGuid().ToString(); @@ -152,11 +181,7 @@ private async Task GenerateFhirFileStorageMetadata() private async Task GenerateDicomFileStorageMetadata() { - var file = new DicomFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString()) - { - Source = "SOURCE", - CalledAeTitle = "AET" - }; + var file = new DicomFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Messaging.Events.DataService.DIMSE, "SOURCE", "AET"); var dataset = new DicomDataset { { DicomTag.PatientID, "PID" }, diff --git a/src/InformaticsGateway/Test/Shared/DicomScpFixture.cs b/src/InformaticsGateway/Test/Shared/DicomScpFixture.cs index 63bbb01bc..28f4a7f8d 100644 --- a/src/InformaticsGateway/Test/Shared/DicomScpFixture.cs +++ b/src/InformaticsGateway/Test/Shared/DicomScpFixture.cs @@ -82,8 +82,8 @@ public void Dispose() public class CStoreScp : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider { - public CStoreScp(INetworkStream stream, Encoding fallbackEncoding, FellowOakDicom.Log.ILogger log, DicomServiceDependencies dicomServiceDependencies) - : base(stream, fallbackEncoding, log, dicomServiceDependencies) + public CStoreScp(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies) + : base(stream, fallbackEncoding, logger, dicomServiceDependencies) { } diff --git a/src/InformaticsGateway/Test/appsettings.json b/src/InformaticsGateway/Test/appsettings.json index 98a52ff80..f3e7bf5c3 100644 --- a/src/InformaticsGateway/Test/appsettings.json +++ b/src/InformaticsGateway/Test/appsettings.json @@ -104,4 +104,4 @@ "InformaticsGatewayServerEndpoint": "http://localhost:5000", "DockerImagePrefix": "monai/informatics-gateway" } -} +} \ No newline at end of file diff --git a/src/InformaticsGateway/Test/packages.lock.json b/src/InformaticsGateway/Test/packages.lock.json index 4d2dc7524..e033fdcf9 100644 --- a/src/InformaticsGateway/Test/packages.lock.json +++ b/src/InformaticsGateway/Test/packages.lock.json @@ -4,9 +4,9 @@ "net6.0": { "coverlet.collector": { "type": "Direct", - "requested": "[3.2.0, )", - "resolved": "3.2.0", - "contentHash": "xjY8xBigSeWIYs4I7DgUHqSNoGqnHi7Fv7/7RZD02rvZyG3hlsjnQKiVKVWKgr9kRKgmV+dEfu8KScvysiC0Wg==" + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" }, "Microsoft.AspNetCore.Mvc.WebApiCompatShim": { "type": "Direct", @@ -22,28 +22,28 @@ }, "Microsoft.EntityFrameworkCore.InMemory": { "type": "Direct", - "requested": "[6.0.15, )", - "resolved": "6.0.15", - "contentHash": "lRL5rTa6iM9SIubc75dTiQd2aYfURgEd7bz5tLA4T+++yOPFPVm9dCQ22ukGhnlJy6Xr9LAQEDOUrJmWDgByTw==", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "NJq3pURTBBHWkHgYkZJlCesZ6udyQIlnS2gU8SdR0xZ5VhW3c90tCCkZel38CgPmq29vWfxLurJLEwroIUokzg==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.15" + "Microsoft.EntityFrameworkCore": "6.0.21" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.5.0, )", - "resolved": "17.5.0", - "contentHash": "IJ4eSPcsRbwbAZehh1M9KgejSy0u3d0wAdkJytfCh67zOaCl5U3ltruUEe15MqirdRqGmm/ngbjeaVeGapSZxg==", + "requested": "[17.7.1, )", + "resolved": "17.7.1", + "contentHash": "o1qyqDOR8eMuQrC1e5EMMcE+Wm3rwES5aHNWaJpi2A5qwVOru23zsdXkndT6hgl79QsJsqKp+/RNcayIzpHjvA==", "dependencies": { - "Microsoft.CodeCoverage": "17.5.0", - "Microsoft.TestPlatform.TestHost": "17.5.0" + "Microsoft.CodeCoverage": "17.7.1", + "Microsoft.TestPlatform.TestHost": "17.7.1" } }, "Moq": { "type": "Direct", - "requested": "[4.18.4, )", - "resolved": "4.18.4", - "contentHash": "IOo+W51+7Afnb0noltJrKxPBSfsgMzTKCw+Re5AMx8l/vBbAbMDOynLik4+lBYIWDJSO0uV7Zdqt7cNb6RZZ+A==", + "requested": "[4.20.69, )", + "resolved": "4.20.69", + "contentHash": "8P/oAUOL8ZVyXnzBBcgdhTsOD1kQbAWfOcMI7KDQO3HqQtzB/0WYLdnMa4Jefv8nu/MQYiiG0IuoJdvG0v0Nig==", "dependencies": { "Castle.Core": "5.1.1" } @@ -80,28 +80,25 @@ }, "xunit": { "type": "Direct", - "requested": "[2.4.2, )", - "resolved": "2.4.2", - "contentHash": "6Mj73Ont3zj2CJuoykVJfE0ZmRwn7C+pTuRP8c4bnaaTFjwNG6tGe0prJ1yIbMe9AHrpDys63ctWacSsFJWK/w==", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "f2V5wuAdoaq0mRTt9UBmPbVex9HcwFYn+y7WaKUz5Xpakcrv7lhtQWBJUWNY4N3Z+o+atDBLyAALM1QWx04C6Q==", "dependencies": { - "xunit.analyzers": "1.0.0", - "xunit.assert": "2.4.2", - "xunit.core": "[2.4.2]" + "xunit.analyzers": "1.2.0", + "xunit.assert": "2.5.0", + "xunit.core": "[2.5.0]" } }, "xunit.runner.visualstudio": { "type": "Direct", - "requested": "[2.4.5, )", - "resolved": "2.4.5", - "contentHash": "OwHamvBdUKgqsXfBzWiCW/O98BTx81UKzx2bieIOQI7CZFE5NEQZGi8PBQGIKawDW96xeRffiNf20SjfC0x9hw==" + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "+Gp9vuC2431yPyKB15YrOTxCuEAErBQUTIs6CquumX1F073UaPHGW0VE/XVJLMh9W4sXdz3TBkcHdFWZrRn2Hw==" }, "Ardalis.GuardClauses": { "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "RemnImQf/BWR8oYqFpdw+hn+b4Q1w+pGujkRiSfjQhMPuiERwGn4UMmQv+6UDE4qbPlnIN+e3e40JkvBhzgfzg==", - "dependencies": { - "JetBrains.Annotations": "2021.3.0" - } + "resolved": "4.1.1", + "contentHash": "+UcJ2s+gf2wMNrwadCaHZV2DMcGgBU1t22A+jm40P4MHQRLy9hcleGy5xdVWd4dXZPa5Vlp4TG5xU2rhoDYrBA==" }, "AspNetCore.HealthChecks.MongoDb": { "type": "Transitive", @@ -114,15 +111,15 @@ }, "AWSSDK.Core": { "type": "Transitive", - "resolved": "3.7.105.20", - "contentHash": "ZHuTxP1J8g91+YSV0YLzm5te5lG+zkiUH/+NDHFpLf1cBD6iw2kUo5AkYEVxfEur1OTdYJxEZ5jDuOBE4pubkg==" + "resolved": "3.7.200.13", + "contentHash": "yiUuhTI8w183euRqhXym1DyhnD/1ccxceRoruCfkIoqY3PAaFgFL8pE4iDLDZa7SUW4M4qZnQ5PMlFr1rrl6zw==" }, "AWSSDK.SecurityToken": { "type": "Transitive", - "resolved": "3.7.101.26", - "contentHash": "/y64ogftqwGa07HNOj2Dh08oqYIgbIyfJFncneHy+fzC54VFhEIN5+pSOHS4Also1SSb9Erk/Knuf3L6jrTVEg==", + "resolved": "3.7.201.9", + "contentHash": "yKlTPrvNeDdzkOX82Ydf7MD09Gk3dK74JWZPRWJ3QIxskWVoNTAyLvfVBzbi+/fGnjf8/qKsSzxT7GHLqds37A==", "dependencies": { - "AWSSDK.Core": "[3.7.105.20, 4.0.0)" + "AWSSDK.Core": "[3.7.200.13, 4.0.0)" } }, "Castle.Core": { @@ -133,13 +130,10 @@ "System.Diagnostics.EventLog": "6.0.0" } }, - "Crc32.NET": { + "CommunityToolkit.HighPerformance": { "type": "Transitive", - "resolved": "1.2.0", - "contentHash": "wNW/huzolu8MNKUnwCVKxjfAlCFpeI8AZVfF46iAWJ1+P6bTU1AZct7VAkDDEjgeeTJCVTkGZaD6jSd/fOiUkA==", - "dependencies": { - "NETStandard.Library": "2.0.0" - } + "resolved": "8.2.0", + "contentHash": "iKzsPiSnXoQUN5AoApYmdfnLn9osNb+YCLWRr5PFmrDEQVIu7OeOyf4DPvNBvbqbYLZCfvHozPkulyv6zBQsFw==" }, "DnsClient": { "type": "Transitive", @@ -168,38 +162,26 @@ }, "fo-dicom": { "type": "Transitive", - "resolved": "5.0.3", - "contentHash": "OPkCQ9+X/fvGRokAAgjR8bOpai04qlnNHmq+LsgI+Kyug3yar2zk6IMOSSvPOLgWe0EG9ScdqH44AGKnviH5Rw==", + "resolved": "5.1.1", + "contentHash": "YraR81u9XuTN7l+pt6HzT0KvuhgWVZ9LCuHMH3zgFfAtv4peT1y+nYMSGwF9YqNP+oZnzh0s0PJ+vJMsTDpGIw==", "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.Extensions.DependencyInjection": "2.2.0", - "Microsoft.Extensions.Options": "2.2.0", - "Microsoft.Toolkit.HighPerformance": "7.1.2", + "CommunityToolkit.HighPerformance": "8.2.0", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", "System.Buffers": "4.5.1", - "System.Text.Encoding.CodePages": "4.6.0", - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.7.2", + "System.Text.Encoding.CodePages": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.7", "System.Threading.Channels": "6.0.0" } }, - "fo-dicom.NLog": { - "type": "Transitive", - "resolved": "5.0.3", - "contentHash": "k35FD+C9IcpTLjCF5tvCkBGUxJ+YvzoBsgb2VAtGQv+aVTu+HyoCnNVqccc4lVE53fbVCwpR3gPiTAnm5fm+KQ==", - "dependencies": { - "NLog": "4.7.11", - "fo-dicom": "5.0.3" - } - }, "HL7-dotnetcore": { "type": "Transitive", - "resolved": "2.35.0", - "contentHash": "1yScq0Ju2O/GPBasnr9/uHziKu3CBgh4nOkgJPWatPLTcP4EzUjjaM2hkgjOBMj8pKO0g687UDnj989MvYRLfA==" - }, - "JetBrains.Annotations": { - "type": "Transitive", - "resolved": "2021.3.0", - "contentHash": "Ddxjs5RRjf+c8m9m++WvhW1lz1bqNhsTjWvCLbQN9bvKbkJeR9MhtfNwKgBRRdG2yLHcXFr5Lf7fsvvkiPaDRg==" + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" }, "Karambolo.Extensions.Logging.File": { "type": "Transitive", @@ -422,31 +404,36 @@ "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" + }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.5.0", - "contentHash": "6FQo0O6LKDqbCiIgVQhJAf810HSjFlOj7FunWaeOGDKxy8DAbpHzPk4SfBTXz9ytaaceuIIeR6hZgplt09m+ig==" + "resolved": "17.7.1", + "contentHash": "NmGwM2ZJy4CAMdJYIp53opUjnXsMbzASX5oQzgxORicJsgz5Lp50fnRI8OmQ/kYNg6dHfr3IjuUoXbsotDX+KA==" }, "Microsoft.CSharp": { "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + "resolved": "4.5.0", + "contentHash": "kaj6Wb4qoMuH3HySFJhxwQfe8R/sJsNJnANrvv8WdFPMoNbKY5htfNscv+LHCu5ipz+49m2e+WQXpLXr9XYemQ==" }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "yE5Q7jJDuGUwS3FMV6N6oz7p7MrtqPrdanLHG6dVXPB3o4KQKLpkPPzUQPByGmBis6wIDGmbWunwjD0vH/qlFQ==", + "resolved": "6.0.21", + "contentHash": "9S+kvYcPyGBqH5KX7sL0d7xYADTUrUVaBz+GZsSx4N4jKh/0mka6IFdeuFYzs3T6wdtHTvzdltcRwucwuTFpdw==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "o51dv+X1Fv1/oPCWtCED4tTov4aBWD59ebkY5BW5K/8hwu+X+AfWpN1/bCBuS/3OPW24RuZmGfigByRMlG/fIA==", + "resolved": "6.0.21", + "contentHash": "XUPcDrn/Vrv9yF4M3b9FYEZvqW1gyS3hfJhFiP0pttuRYnGRB+y3/6g/9k0GIoU62+XkxGa78l1JUccq1uvAXQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.15", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.15", + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.21", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.21", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Logging": "6.0.0", @@ -456,39 +443,39 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "seE5q7/0R1LmWiQcd5pZYzlY8WdVojv2tk+5o0p4HrEvliOysomjIOYVEEHJnK9NwXqHBcZra4b+RwzgWYdbzA==" + "resolved": "6.0.21", + "contentHash": "GlNsy7qoFnCxgZlPpb8H/Srq1juOiV6W5XaijSA0+h8V0yn1VJ0owjb01If3di3Covs/8682A+ByTFjmEUxePA==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "0ZKFq5irkVVyPJmQDorRsWxXy85wKm+UPO8J6pf2h1ggGl1CkhlXa+bteM8NBo++Cfylv8cBSo8ZfQZHV57fIg==" + "resolved": "6.0.21", + "contentHash": "gS8tH419vOY2kEyqEZBX8VnXWmtHaor7gVx6zVaXCsEyQurGR/aVB++IZ62vzeQFS9R46LbNY6D6bqEA6j3iCg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "ouk4es/CzwxjXl33mb2hJzitluc2CD9rujZVBaUy3w3fn8qMjlktMOhf5mIAS7e3sreBikOBwaxp9/y/N/O2NQ==", + "resolved": "6.0.21", + "contentHash": "Ev5FM2KpXJu7+Dm9qvLf1FhSJMytrhSXho92Vompmgeiz3p4InldluidmKKmv8nZQAjs9dTCUUyvk1pxQjysaQ==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.15", + "Microsoft.EntityFrameworkCore": "6.0.21", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "4oRXU58XmoDkK27wDMmIrZG9yaOYw8URmWNQzGkfO0ZCpELX/bx6rtb99eoBOOzA+a0QYoTLlugZB7MyM1XDbw==", + "resolved": "6.0.21", + "contentHash": "iAs1F5gxEQRRGNHDKJ6ZtoSbOAWcjdk+mABEIy2vRLeACp7xBPdQRQdJnENmxykkBgOVef73RpU3xVdDcn8Omg==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.15", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.21", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "30gMAP29sWQ9yTSM/VXknmv8BcH9AVO+QHCpoDoAlzPnmL6STjJ5jihlOp1mvErGVTkEgnaIxmv4j3gX6knFRw==", + "resolved": "6.0.21", + "contentHash": "2If1Lt04gD+KrKPFbMUeUzB8Av/EGJJFxNLGfC/CKLgy8+jAYsamyQ/Hux+93XCajJxFLnJimqSg+bBBvXX+2g==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.15", - "Microsoft.EntityFrameworkCore.Relational": "6.0.15", + "Microsoft.Data.Sqlite.Core": "6.0.21", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -542,24 +529,6 @@ "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Configuration.CommandLine": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "3nL1qCkZ1Oxx14ZTzgo4MmlO7tso7F+TtMZAY2jUAtTLyAcDp+EDjk3RqafoKiNaePyPvvlleEcBxh3b2Hzl1g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "pnyXV1LFOsYjGveuC07xp0YHIyGq7jRq5Ncb5zrrIieMLWVwgMyYxcOH0jTnBedDT4Gh1QinSqsjqzcieHk1og==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" - } - }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "6.0.0", @@ -584,17 +553,6 @@ "System.Text.Json": "6.0.0" } }, - "Microsoft.Extensions.Configuration.UserSecrets": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "Fy8yr4V6obi7ZxvKYI1i85jqtwMq8tqyxQVZpRSkgeA8enqy/KvBIMdcuNdznlxQMZa72mvbHqb7vbg4Pyx95w==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0" - } - }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "6.0.1", @@ -623,28 +581,28 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "crR/15PKDgVIQmH9uGJuQVg4RGbaxwG3cseRRMisPG/2LkiQV71EkNRGPV4cI61Waywc1Wn5sYXE8bo2qCf+/Q==", + "resolved": "6.0.21", + "contentHash": "1Qf/tEg6IlzbvCxrc+pZE+ZGrajBdB/+V2+abeAu6lg8wXGHmO8JtnrNqwc5svSbcz3udxinUPyH3vw6ZujKbg==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.3", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" } }, "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "LmB5kbbc0Sr+XvnYj8tReZzubS50h1g463zpbnnjqT/k6fM8/od9hFCBj52dorXfp/DDfm5+rUdKaPRUsX70Jg==" + "resolved": "6.0.21", + "contentHash": "5FSA1euCRtbRqVgTn2ahgCG9Cy29UQXAZMCJLUlrQQaC5rko0+d/aq9SiFGIDP7cvoWUsatrlNdfc6UyOMV5aA==" }, "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "jIWboFkp6O/G3wF6JwQq8A5AR5TcZbCRzXdBhaYgVAGiWexb95/2JkytGFrJJ44pBiWO76jpOT4vShGLAgf1HQ==", + "resolved": "6.0.21", + "contentHash": "6StjSICTiNdXK9NiQx0jpmsfJhSsXekjfJt8r/3K9qUx9dxVF8V2hhhIxRnZt8HM+4YagFLejNCD6hFUAnx9pw==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.15", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.15", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15" + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -670,34 +628,6 @@ "resolved": "6.0.0", "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" }, - "Microsoft.Extensions.Hosting": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "hbmizc9KPWOacLU8Z8YMaBG6KWdZFppczYV/KwnPGU/8xebWxQxdDeJmLOgg968prb7g2oQgnp6JVLX6lgby8g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Binder": "6.0.0", - "Microsoft.Extensions.Configuration.CommandLine": "6.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Microsoft.Extensions.Logging.Console": "6.0.0", - "Microsoft.Extensions.Logging.Debug": "6.0.0", - "Microsoft.Extensions.Logging.EventLog": "6.0.0", - "Microsoft.Extensions.Logging.EventSource": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" - } - }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -708,17 +638,6 @@ "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "15+pa2G0bAMHbHewaQIdr/y6ag2H3yh4rd9hTXavtWDzQBkvpe2RMqFg8BxDpcQWssmjmBApGPcw93QRz6YcMg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" - } - }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "6.0.0", @@ -733,8 +652,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "6.0.3", - "contentHash": "SUpStcdjeBbdKjPKe53hVVLkFjylX0yIXY8K+xWa47+o1d+REDyOMZjHZa+chsQI1K9qZeiHWk9jos0TFU7vGg==" + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", @@ -751,55 +670,6 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0" } }, - "Microsoft.Extensions.Logging.Console": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "gsqKzOEdsvq28QiXFxagmn1oRB9GeI5GgYCkoybZtQA0IUb7QPwf1WmN3AwJeNIsadTvIFQCiVK0OVIgKfOBGg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.Debug": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "M9g/JixseSZATJE9tcMn9uzoD4+DbSglivFqVx8YkRJ7VVPmnvCEbOZ0AAaxsL1EKyI4cz07DXOOJExxNsUOHw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "rlo0RxlMd0WtLG3CHI0qOTp6fFn7MvQjlrCjucA31RqmiMFCZkF8CHNbe8O7tbBIyyoLGWB1he9CbaA5iyHthg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.EventSource": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "BeDyyqt7nkm/nr+Gdk+L8n1tUT/u33VkbXAOesgYSNsxDM9hJ1NOBGoZfj9rCbeD2+9myElI6JOVVFmnzgeWQA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", "resolved": "2.2.0", @@ -877,8 +747,8 @@ }, "Microsoft.Net.Http.Headers": { "type": "Transitive", - "resolved": "2.2.8", - "contentHash": "wHdwMv0QDDG2NWDSwax9cjkeQceGC1Qq53a31+31XpvTXVljKXRjWISlMoS/wZYKiqdqzuEvKFKwGHl+mt2jCA==", + "resolved": "2.2.0", + "contentHash": "iZNkjYqlo8sIOI0bQfpsSoMTmB/kyvmV2h225ihyZT33aTp48ZpF6qYnXxzSXmHt8DpBAwBTX+1s1UFLbYfZKg==", "dependencies": { "Microsoft.Extensions.Primitives": "2.2.0", "System.Buffers": "4.5.0" @@ -891,8 +761,8 @@ }, "Microsoft.NETCore.Targets": { "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" }, "Microsoft.OpenApi": { "type": "Transitive", @@ -901,26 +771,31 @@ }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.5.0", - "contentHash": "QwiBJcC/oEA1kojOaB0uPWOIo4i6BYuTBBYJVhUvmXkyYqZ2Ut/VZfgi+enf8LF8J4sjO98oRRFt39MiRorcIw==", + "resolved": "17.7.1", + "contentHash": "nDmV03yHIdAiG5J3ZEjMyJM2XDjmWORuKgbrGzqlAipBEjUuy5Z5S7WwSqUv9OiaUrtCn9dNYmjfMELUi08leQ==", "dependencies": { - "NuGet.Frameworks": "5.11.0", + "NuGet.Frameworks": "6.5.0", "System.Reflection.Metadata": "1.6.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.5.0", - "contentHash": "X86aikwp9d4SDcBChwzQYZihTPGEtMdDk+9t64emAl7N0Tq+OmlLAoW+Rs+2FB2k6QdUicSlT4QLO2xABRokaw==", + "resolved": "17.7.1", + "contentHash": "WCU1NyBarz0tih+I9K5OWN1dVo3z562Iek/VAqWNWRFWw1GeUGqB61iixrBvZO77sjTtBc1cXO8H95uImfmEdw==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.5.0", + "Microsoft.TestPlatform.ObjectModel": "17.7.1", "Newtonsoft.Json": "13.0.1" } }, - "Microsoft.Toolkit.HighPerformance": { + "Microsoft.Win32.Primitives": { "type": "Transitive", - "resolved": "7.1.2", - "contentHash": "cezzRky0BUJyYmSrcQUcX8qAv90JfUwCqWEbqfWZLHyeANo9/LWgW6y50pqbyc8r8SPXVsu2GNH98fB3VxrnvA==" + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } }, "Microsoft.Win32.Registry": { "type": "Transitive", @@ -933,41 +808,33 @@ }, "Minio": { "type": "Transitive", - "resolved": "4.0.7", - "contentHash": "gmd+B4YHaE+cz8TCPIzkhov3t30VovGfyf6vMxqXd/iF/cziKmXKJJXv2E9kcHvbGwVHhMWtvk7fXWOzHcU4uw==", + "resolved": "5.0.0", + "contentHash": "7tZj90WEuuH60RAP4wBYexjMuJOhCnK7I46hCiX3CtZPackHisLZ8aAJmn3KlwbUX22dBDphwemD+h37vet8Qw==", "dependencies": { - "Crc32.NET": "1.2.0", - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.1", - "System.Net.Http": "4.3.4", - "System.Net.Primitives": "4.3.1", - "System.Reactive.Linq": "5.0.0", - "System.ValueTuple": "4.4.0" + "CommunityToolkit.HighPerformance": "8.1.0", + "System.IO.Hashing": "7.0.0", + "System.Reactive.Linq": "5.0.0" } }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "0.1.22", - "contentHash": "pFZBuV3TaZvZJz8wTib8G/Doa/XHkM8uv12VtuLkQc7lI8AbJmH1eIHnpRliyuKPmw7VMhOMiS7JhyqutC0uvQ==", + "resolved": "1.0.0", + "contentHash": "Xr1V3ZrSJByfUP4w+aiOAqC7Uzt1GqRXj35qSTQs9C1oI4gCiBN4wnre0SSvoA7vHQNZPGWNWXtiqbI7Cov3Mg==", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.Extensions.Configuration": "6.0.1", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.14", - "Microsoft.Extensions.Logging": "6.0.0", + "Ardalis.GuardClauses": "4.1.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", "Newtonsoft.Json": "13.0.3", - "System.ComponentModel.Annotations": "5.0.0", "System.IO.Abstractions": "17.2.3" } }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Transitive", - "resolved": "0.1.22", - "contentHash": "ZJEHtM4NaX8UzvG+w1coKOivbCecoU6hx8g06PGKkg6giIeLGqCi2FDkP89kIPq7Kz1RB9cLVvYdXY9Rs+ZDSg==", + "resolved": "1.0.0", + "contentHash": "1UiWYO+EjNZSFrL/SUElqmBo3TktG+XiCXm8oyXheEWz/CuZS2hhepYB4BDz7XAohUqt2/Hv7wpLiaauiaIFZg==", "dependencies": { - "Monai.Deploy.Messaging": "0.1.22", - "Polly": "7.2.3", - "RabbitMQ.Client": "6.4.0", - "System.Collections.Concurrent": "4.3.0" + "Monai.Deploy.Messaging": "1.0.0", + "Polly": "7.2.4", + "RabbitMQ.Client": "6.5.0" } }, "Monai.Deploy.Security": { @@ -986,70 +853,64 @@ }, "Monai.Deploy.Storage": { "type": "Transitive", - "resolved": "0.2.16", - "contentHash": "UVu9yyiJf4mYlz1H6g29LXdYv5RRFcttudTczA3w3SuGQ5u3fEIRbDaWuTjFjaaLor+IuibGaob6c1L6ACH7kg==", + "resolved": "0.2.18", + "contentHash": "+1JX7QDgVEMqYA0/M1QMr1gtXRC6lEuhBtLfJXWi6cEgh9kOPE0KiHd1AWI7PxBgBbsEBZaNQSvWqShlwcu6bA==", "dependencies": { - "AWSSDK.SecurityToken": "3.7.101.26", - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.Extensions.Configuration": "6.0.1", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.15", - "Microsoft.Extensions.Logging": "6.0.0", - "Monai.Deploy.Storage.S3Policy": "0.2.16", + "AWSSDK.SecurityToken": "3.7.201.9", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Monai.Deploy.Storage.S3Policy": "0.2.18", "System.IO.Abstractions": "17.2.3" } }, "Monai.Deploy.Storage.MinIO": { "type": "Transitive", - "resolved": "0.2.16", - "contentHash": "/J8fylX91D/ePPCHTCgY11nPdADR5MZO239ZZWrK7ZO2ngAvKRmTB4Q6q6JbSErDF1hSk11HGhWCWNDsnK1rAQ==", + "resolved": "0.2.18", + "contentHash": "0sHLiT0qU2Fg5O+AF8UDqzsJEYztUAFZeOPh4kOLC4bckhb+wSsuv7VcAXWtR3BOY6TxaMVVUJ+EK/o5mCp3tQ==", "dependencies": { - "AWSSDK.SecurityToken": "3.7.101.26", - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Minio": "4.0.7", - "Monai.Deploy.Storage": "0.2.16", - "Monai.Deploy.Storage.S3Policy": "0.2.16" + "Minio": "5.0.0", + "Monai.Deploy.Storage": "0.2.18", + "Monai.Deploy.Storage.S3Policy": "0.2.18" } }, "Monai.Deploy.Storage.S3Policy": { "type": "Transitive", - "resolved": "0.2.16", - "contentHash": "KfiAW28llXSlDLgGwAeBCZGns9/5D/wHVOKt2NxzeiJpp0fR3VspQIbuHO1cbn4GcDM7tL0Se4gR494rHGoz4g==", + "resolved": "0.2.18", + "contentHash": "+b0nDnf4OoajdH2hB02elRC6G+GjlYnxJC+F3dGbUUXGMtPApzs8c8s/EG4fKzihxsVovJtqnJl7atcaPyl12Q==", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", + "Ardalis.GuardClauses": "4.1.1", "Newtonsoft.Json": "13.0.3" } }, "MongoDB.Bson": { "type": "Transitive", - "resolved": "2.19.1", - "contentHash": "4FSR3eAbJEYMmvQ1pNFImUpFGtGHT+kEw/Yw/KZjxC9iFMj1XcZC08wMbezgRga2F9tNNFG2vDqh9zt01GinMA==", + "resolved": "2.21.0", + "contentHash": "QT+D1I3Jz6r6S6kCgJD1L9dRCLVJCKlkGRkA+tJ7uLpHRmjDNcNKy4D1T+L9gQrjl95lDN9PHdwEytdvCW/jzA==", "dependencies": { + "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "5.0.0" } }, "MongoDB.Driver": { "type": "Transitive", - "resolved": "2.19.1", - "contentHash": "EeQnUCIzRmXg20jwHSM9uvw67nrEMpINKsJDF9Y8xFh/8WFWD9QjZyyJLZgUoFUSz9pUAbyLfQj+ctJYbn8gxg==", + "resolved": "2.21.0", + "contentHash": "VxKj1wuhadiXhaXkykCWRgsYOysdaOYJ202hJFz25UjkrqC/tHA8RS4hdS5HYfGWoI//fypBXnxZCkEjXLXdfw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "2.0.0", - "MongoDB.Bson": "2.19.1", - "MongoDB.Driver.Core": "2.19.1", - "MongoDB.Libmongocrypt": "1.7.0" + "MongoDB.Bson": "2.21.0", + "MongoDB.Driver.Core": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0" } }, "MongoDB.Driver.Core": { "type": "Transitive", - "resolved": "2.19.1", - "contentHash": "+T4+vNZHCjp7qoOoNE8hf8VjnwxZttTOHTqv0jibJ4WSnM6lnXZBP4wBOjIKDF3J4aQffvtaZtIt4UWDOV+yAw==", + "resolved": "2.21.0", + "contentHash": "Ac44U3bQfinmdH5KNFjTidJe9LKW87SxkXJ3YuIUJQMITEc4083YF1yvjJxaSeYF9er0YgHSmwhHpsZv0Fwplg==", "dependencies": { "AWSSDK.SecurityToken": "3.7.100.14", "DnsClient": "1.6.1", "Microsoft.Extensions.Logging.Abstractions": "2.0.0", - "MongoDB.Bson": "2.19.1", - "MongoDB.Libmongocrypt": "1.7.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0", "SharpCompress": "0.30.1", "Snappier": "1.0.0", "System.Buffers": "4.5.1", @@ -1058,15 +919,58 @@ }, "MongoDB.Libmongocrypt": { "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "p9+peTZX63nGHskOLhvhfBtrknxNg1RzXepE07rPozuCGz27bMjCcQyvn2YByg0L3YEcNWdTmI4BlnG/5RF+5Q==" + "resolved": "1.8.0", + "contentHash": "fgNw8Dxpkq7mpoaAYes8cfnPRzvFIoB8oL9GPXwi3op/rONftl0WAeg4akRLcxfoVuUvuUO2wGoVBr3JzJ7Svw==" }, "NETStandard.Library": { "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" } }, "Newtonsoft.Json": { @@ -1085,60 +989,60 @@ }, "NLog": { "type": "Transitive", - "resolved": "5.1.3", - "contentHash": "rB8hwjBf1EZCfG5iPfsv3gPksLoJLr1cOrt7PBbJu6VpJgwYJchDzTUT1dhNDdPv0QakXJQJOhE59ErupcznQQ==" + "resolved": "5.2.3", + "contentHash": "rHTNRtQF5qYqLutSR9ldUWXglKym/KA1R6GKw4JtDvza8i5+kgfmeKH75Ccn1noeJIOjHLXorphMxKk3EiN2tg==" }, "NLog.Extensions.Logging": { "type": "Transitive", - "resolved": "5.2.3", - "contentHash": "TB8zPGV2nVpvWq5C8zIVHPSmnzOHMrXppjsAwHcuJq1Ehs8sC0llnAv5Ysf5Lf/vew9amV/+01MohtRFSDzKdQ==", + "resolved": "5.3.3", + "contentHash": "o3V1oUr0izjhU1djuVqN5JdmNUGmunTs3Amjhumt/nxva8kG9QWjOdba+ciwkP//QOjv+KkGklZtI9o4qz50hQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", "Microsoft.Extensions.Logging": "6.0.0", - "NLog": "5.1.3" + "NLog": "5.2.3" } }, "NLog.Web.AspNetCore": { "type": "Transitive", - "resolved": "5.2.3", - "contentHash": "uP0KekbkswuMjo1dbaqu20TxH2Dc3ox2qJDIi837ob2Fq/BliZHuQY9nJdM3UArVrLrsl+xxsx0D6h8m3fOufg==", + "resolved": "5.3.3", + "contentHash": "ub8LOAbIGIPtv9nMAdZXlxUvszau6p2Svmeo8mhJFD+PQDMnI6PFc5IID1Jj3c1Lv8sxKVL7vRCsaWdTrmnrFw==", "dependencies": { - "NLog.Extensions.Logging": "5.2.3" + "NLog.Extensions.Logging": "5.3.3" } }, "NuGet.Frameworks": { "type": "Transitive", - "resolved": "5.11.0", - "contentHash": "eaiXkUjC4NPcquGWzAGMXjuxvLwc6XGKMptSyOGQeT0X70BUZObuybJFZLA0OfTdueLd3US23NBPTBb6iF3V1Q==" + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" }, "Polly": { "type": "Transitive", - "resolved": "7.2.3", - "contentHash": "DeCY0OFbNdNxsjntr1gTXHJ5pKUwYzp04Er2LLeN3g6pWhffsGuKVfMBLe1lw7x76HrPkLxKEFxBlpRxS2nDEQ==" + "resolved": "7.2.4", + "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA==" }, "RabbitMQ.Client": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "1znR1gGU+xYVSpO5z8nQolcUKA/yydnxQn7Ug9+RUXxTSLMm/eE58VKGwahPBjELXvDnX0k/kBrAitFLRjx9LA==", + "resolved": "6.5.0", + "contentHash": "9hY5HiWPtCla1/l0WmXmLnqoX7iKE3neBQUWnetIJrRpOvTbO//XQfQDh++xgHCshL40Kv/6bR0HDkmJz46twg==", "dependencies": { - "System.Memory": "4.5.4", - "System.Threading.Channels": "4.7.1" + "System.Memory": "4.5.5", + "System.Threading.Channels": "7.0.0" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" }, "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" }, "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" }, "runtime.native.System": { "type": "Transitive", @@ -1149,6 +1053,15 @@ "Microsoft.NETCore.Targets": "1.1.0" } }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, "runtime.native.System.Net.Http": { "type": "Transitive", "resolved": "4.3.0", @@ -1168,30 +1081,30 @@ }, "runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" }, "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" }, "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { "type": "Transitive", @@ -1200,28 +1113,28 @@ }, "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" }, "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" }, "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" }, "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" }, "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" }, "SharpCompress": { "type": "Transitive", @@ -1284,6 +1197,14 @@ "resolved": "6.5.0", "contentHash": "OvbvxX+wL8skxTBttcBsVxdh73Fag4xwqEU2edh4JMn7Ws/xJHnY/JB1e9RoCb6XpDxUF3hD9A0Z1lEUx40Pfw==" }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Buffers": { "type": "Transitive", "resolved": "4.5.1", @@ -1324,10 +1245,17 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "System.ComponentModel.Annotations": { + "System.Console": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } }, "System.Diagnostics.Debug": { "type": "Transitive", @@ -1352,6 +1280,16 @@ "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.Diagnostics.Tracing": { "type": "Transitive", "resolved": "4.3.0", @@ -1422,6 +1360,44 @@ "resolved": "17.2.3", "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.IO.FileSystem": { "type": "Transitive", "resolved": "4.3.0", @@ -1445,6 +1421,11 @@ "System.Runtime": "4.3.0" } }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "sDnWM0N3AMCa86LrKTWeF3BZLD2sgWyYUc7HL6z4+xyDZNQRwzmxbo4qP2rX2MqC+Sy1/gOSRDah5ltxY5jPxw==" + }, "System.Linq": { "type": "Transitive", "resolved": "4.3.0", @@ -1465,17 +1446,41 @@ "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "System.Net.Http": { "type": "Transitive", - "resolved": "4.3.4", - "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Platforms": "1.1.0", "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.DiagnosticSource": "4.3.0", @@ -1500,20 +1505,45 @@ "System.Threading.Tasks": "4.3.0", "runtime.native.System": "4.3.0", "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, "System.Net.Primitives": { "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "OHzPhSme78BbmLe9UBxHM69ZYjClS5URuhce6Ta4ikiLgaUGiG/X84fZpI6zy7CsUH5R9cYzI2tv9dWPqdTkUg==", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "System.Runtime": "4.3.1", + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", "System.Runtime.Handles": "4.3.0" } }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Reactive": { "type": "Transitive", "resolved": "5.0.0", @@ -1540,6 +1570,50 @@ "System.Runtime": "4.3.0" } }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Reflection.Metadata": { "type": "Transitive", "resolved": "1.6.0", @@ -1555,6 +1629,15 @@ "System.Runtime": "4.3.0" } }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Resources.ResourceManager": { "type": "Transitive", "resolved": "4.3.0", @@ -1569,11 +1652,11 @@ }, "System.Runtime": { "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" } }, "System.Runtime.CompilerServices.Unsafe": { @@ -1614,6 +1697,20 @@ "System.Runtime.Handles": "4.3.0" } }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, "System.Runtime.Numerics": { "type": "Transitive", "resolved": "4.3.0", @@ -1782,10 +1879,21 @@ }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "OCUK9C/U97+UheVwo+JE+IUcKySUE3Oe+BcHhVtQrvmKSUFLrUDO8B5zEPRL6mBGbczxZp4w1boSck6/fw4dog==", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", "dependencies": { - "Microsoft.NETCore.Platforms": "3.0.0" + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" } }, "System.Text.Encodings.Web": { @@ -1805,6 +1913,14 @@ "System.Text.Encodings.Web": "6.0.0" } }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Threading": { "type": "Transitive", "resolved": "4.3.0", @@ -1816,8 +1932,8 @@ }, "System.Threading.Channels": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" }, "System.Threading.Tasks": { "type": "Transitive", @@ -1829,20 +1945,61 @@ "System.Runtime": "4.3.0" } }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" - }, "System.Threading.Tasks.Extensions": { "type": "Transitive", "resolved": "4.5.4", "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" }, - "System.ValueTuple": { + "System.Threading.Timer": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "BahUww/+mdP4ARCAh2RQhQTg13wYLVrBb9SYVgW8ZlrwjraGCXHGjo0oIiUfZ34LUZkMMR+RAzR7dEY4S1HeQQ==" + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } }, "xunit.abstractions": { "type": "Transitive", @@ -1851,30 +2008,30 @@ }, "xunit.analyzers": { "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "BeO8hEgs/c8Ls2647fPfieMngncvf0D0xYNDfIO59MolxtCtVjFRd6SRc+7tj8VMqkVOuJcnc9eh4ngI2cAmLQ==" + "resolved": "1.2.0", + "contentHash": "d3dehV/DASLRlR8stWQmbPPjfYC2tct50Evav+OlsJMkfFqkhYvzO1k0s81lk0px8O0knZU/FqC8SqbXOtn+hw==" }, "xunit.assert": { "type": "Transitive", - "resolved": "2.4.2", - "contentHash": "pxJISOFjn2XTTi1mcDCkRZrTFb9OtRRCtx2kZFNF51GdReLr1ls2rnyxvAS4JO247K3aNtflvh5Q0346K5BROA==", + "resolved": "2.5.0", + "contentHash": "wN84pKX5jzfpgJ0bB6arrCA/oelBeYLCpnQ9Wj5xGEVPydKzVSDY5tEatFLHE/rO0+0RC+I4H5igGE118jRh1w==", "dependencies": { "NETStandard.Library": "1.6.1" } }, "xunit.core": { "type": "Transitive", - "resolved": "2.4.2", - "contentHash": "KB4yGCxNqIVyekhJLXtKSEq6BaXVp/JO3mbGVE1hxypZTLEe7h+sTbAhpA+yZW2dPtXTuiW+C1B2oxxHEkrmOw==", + "resolved": "2.5.0", + "contentHash": "dnV0Mn2s1C0y2m33AylQyMkEyhBQsL4R0302kwSGiEGuY3JwzEmhTa9pnghyMRPliYSs4fXfkEAP+5bKXryGFg==", "dependencies": { - "xunit.extensibility.core": "[2.4.2]", - "xunit.extensibility.execution": "[2.4.2]" + "xunit.extensibility.core": "[2.5.0]", + "xunit.extensibility.execution": "[2.5.0]" } }, "xunit.extensibility.core": { "type": "Transitive", - "resolved": "2.4.2", - "contentHash": "W1BoXTIN1C6kpVSMw25huSet25ky6IAQUNovu3zGOGN/jWnbgSoTyCrlIhmXSg0tH5nEf8q7h3OjNHOjyu5PfA==", + "resolved": "2.5.0", + "contentHash": "xRm6NIV3i7I+LkjsAJ91Xz2fxJm/oMEi2CYq1G5HlGTgcK1Zo2wNbLO6nKX1VG5FZzXibSdoLwr/MofVvh3mFA==", "dependencies": { "NETStandard.Library": "1.6.1", "xunit.abstractions": "2.0.3" @@ -1882,11 +2039,11 @@ }, "xunit.extensibility.execution": { "type": "Transitive", - "resolved": "2.4.2", - "contentHash": "CZmgcKkwpyo8FlupZdWpJCryrAOWLh1FBPG6gmVZuPQkGQsim/oL4PcP4nfrC2hHgXUFtluvaJ0Sp9PQKUMNpg==", + "resolved": "2.5.0", + "contentHash": "7+v2Bvp+1ew1iMGQVb1glICi8jcNdHbRUX6Ru0dmJBViGdjiS7kyqcX2VxleQhFbKNi+WF0an7/TeTXD283RlQ==", "dependencies": { "NETStandard.Library": "1.6.1", - "xunit.extensibility.core": "[2.4.2]" + "xunit.extensibility.core": "[2.5.0]" } }, "ZstdSharp.Port": { @@ -1897,132 +2054,108 @@ "monai.deploy.informaticsgateway": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", - "DotNext.Threading": "4.7.4", - "HL7-dotnetcore": "2.35.0", - "Karambolo.Extensions.Logging.File": "3.4.0", - "Microsoft.EntityFrameworkCore": "6.0.15", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "6.0.15", - "Microsoft.Extensions.Hosting": "6.0.1", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Console": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Monai.Deploy.InformaticsGateway.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Common": "1.0.0", - "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "1.0.0", - "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "1.0.0", - "Monai.Deploy.Messaging.RabbitMQ": "0.1.22", - "Monai.Deploy.Security": "0.1.3", - "Monai.Deploy.Storage": "0.2.16", - "Monai.Deploy.Storage.MinIO": "0.2.16", - "NLog": "5.1.3", - "NLog.Web.AspNetCore": "5.2.3", - "Polly": "7.2.3", - "Swashbuckle.AspNetCore": "6.5.0", - "fo-dicom": "5.0.3", - "fo-dicom.NLog": "5.0.3" + "DotNext.Threading": "[4.7.4, )", + "HL7-dotnetcore": "[2.36.0, )", + "Karambolo.Extensions.Logging.File": "[3.4.0, )", + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.DicomWeb.Client": "[1.0.0, )", + "Monai.Deploy.Messaging.RabbitMQ": "[1.0.0, )", + "Monai.Deploy.Security": "[0.1.3, )", + "Monai.Deploy.Storage.MinIO": "[0.2.18, )", + "NLog.Web.AspNetCore": "[5.3.3, )", + "Swashbuckle.AspNetCore": "[6.5.0, )" } }, "monai.deploy.informaticsgateway.api": { "type": "Project", "dependencies": { - "Macross.Json.Extensions": "3.0.0", - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.15", - "Monai.Deploy.InformaticsGateway.Common": "1.0.0", - "Monai.Deploy.Messaging": "0.1.22", - "Monai.Deploy.Storage": "0.2.16" + "Macross.Json.Extensions": "[3.0.0, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.21, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", + "Monai.Deploy.Messaging": "[1.0.0, )", + "Monai.Deploy.Storage": "[0.2.18, )", + "fo-dicom": "[5.1.1, )" } }, "monai.deploy.informaticsgateway.client.common": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", - "System.Text.Json": "6.0.7" + "Ardalis.GuardClauses": "[4.1.1, )" } }, "monai.deploy.informaticsgateway.common": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", - "System.IO.Abstractions": "17.2.3", - "System.Threading.Tasks.Dataflow": "6.0.0", - "fo-dicom": "5.0.3" + "Ardalis.GuardClauses": "[4.1.1, )", + "System.IO.Abstractions": "[17.2.3, )" } }, "monai.deploy.informaticsgateway.configuration": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "6.0.3", - "Microsoft.Extensions.Options": "6.0.0", - "Monai.Deploy.InformaticsGateway.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Common": "1.0.0", - "Monai.Deploy.Messaging": "0.1.22", - "Monai.Deploy.Storage": "0.2.16", - "System.IO.Abstractions": "17.2.3" + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )" } }, "monai.deploy.informaticsgateway.database": { "type": "Project", "dependencies": { - "AspNetCore.HealthChecks.MongoDb": "6.0.2", - "Microsoft.EntityFrameworkCore": "6.0.15", - "Microsoft.Extensions.Configuration": "6.0.1", - "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "6.0.15", - "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", - "Monai.Deploy.InformaticsGateway.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database.MongoDB": "1.0.0" + "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )", + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.21, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )", + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.EntityFramework": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.MongoDB": "[1.0.0, )" } }, "monai.deploy.informaticsgateway.database.api": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.15", - "Monai.Deploy.InformaticsGateway.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0", - "Polly": "7.2.3" + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )" } }, "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.15", - "Microsoft.EntityFrameworkCore.Sqlite": "6.0.15", - "Microsoft.Extensions.Configuration": "6.0.1", - "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Monai.Deploy.InformaticsGateway.Api": "1.0.0", - "Monai.Deploy.InformaticsGateway.Configuration": "1.0.0", - "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0" + "Microsoft.EntityFrameworkCore": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.21, )", + "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", + "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", + "Polly": "[7.2.4, )" } }, "monai.deploy.informaticsgateway.database.mongodb": { "type": "Project", "dependencies": { - "Monai.Deploy.InformaticsGateway.Database.Api": "1.0.0", - "MongoDB.Driver": "2.19.1", - "MongoDB.Driver.Core": "2.19.1" + "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", + "MongoDB.Driver": "[2.21.0, )", + "Polly": "[7.2.4, )" } }, "monai.deploy.informaticsgateway.dicomweb.client": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.AspNet.WebApi.Client": "5.2.9", - "Microsoft.Extensions.Http": "6.0.0", - "Microsoft.Net.Http.Headers": "2.2.8", - "Monai.Deploy.InformaticsGateway.Client.Common": "1.0.0", - "System.Linq.Async": "6.0.1", - "fo-dicom": "5.0.3" + "Microsoft.AspNet.WebApi.Client": "[5.2.9, )", + "Monai.Deploy.InformaticsGateway.Client.Common": "[1.0.0, )", + "System.Linq.Async": "[6.0.1, )", + "fo-dicom": "[5.1.1, )" + } + }, + "monai.deploy.informaticsgateway.test.plugins": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )" } } } diff --git a/src/InformaticsGateway/appsettings.Development.json b/src/InformaticsGateway/appsettings.Development.json index 0f7a87d93..544cb9858 100644 --- a/src/InformaticsGateway/appsettings.Development.json +++ b/src/InformaticsGateway/appsettings.Development.json @@ -16,16 +16,16 @@ }, "messaging": { "publisherSettings": { - "endpoint": "localhost", - "username": "admin", - "password": "admin", + "endpoint": "127.0.0.1", + "username": "rabbitmq", + "password": "rabbitmq", "virtualHost": "monaideploy", "exchange": "monaideploy" }, "subscriberSettings": { - "endpoint": "localhost", - "username": "admin", - "password": "admin", + "endpoint": "127.0.0.1", + "username": "rabbitmq", + "password": "rabbitmq", "virtualHost": "monaideploy", "exchange": "monaideploy", "exportRequestQueue": "export_tasks" @@ -35,7 +35,7 @@ "concurrentUploads": 5, "localTemporaryStoragePath": "./payloads", "settings": { - "endpoint": "localhost:9000", + "endpoint": "127.0.0.1:9000", "accessKey": "minioadmin", "accessToken": "minioadmin", "securedConnection": false diff --git a/src/InformaticsGateway/appsettings.json b/src/InformaticsGateway/appsettings.json old mode 100644 new mode 100755 index 0b825c562..3eb753c58 --- a/src/InformaticsGateway/appsettings.json +++ b/src/InformaticsGateway/appsettings.json @@ -2,11 +2,11 @@ "MonaiDeployAuthentication": { "bypassAuthentication": false, "openId": { - "realm": "http://localhost:8080/realms/monai-test/", - "realmKey": "G6GIR03BEJMlevUtAuZ1ao1SN6IHyUXR", - "clientId": "monai-app", - "audiences": [ "monai-app-test" ], - "roleClaimType": "roles", + "realm": "{realm}", + "realmKey": "{realm-secret-key}", + "clientId": "{client-id}", + "audiences": [ "{audiences}" ], + "roleClaimType": "{roles}", "claimMappings": { "userClaims": [ { @@ -93,6 +93,14 @@ "maximumNumberOfConnections": 10, "clientTimeout": 60000, "sendAck": true + }, + "dicomWeb": { + "plugins": [] + }, + "plugins": { + "remoteApp": { + "ReplaceTags": "AccessionNumber" + } } }, "Kestrel": { diff --git a/src/InformaticsGateway/packages.lock.json b/src/InformaticsGateway/packages.lock.json index 5c5e8d40f..302fc9965 100644 --- a/src/InformaticsGateway/packages.lock.json +++ b/src/InformaticsGateway/packages.lock.json @@ -2,15 +2,6 @@ "version": 1, "dependencies": { "net6.0": { - "Ardalis.GuardClauses": { - "type": "Direct", - "requested": "[4.0.1, )", - "resolved": "4.0.1", - "contentHash": "RemnImQf/BWR8oYqFpdw+hn+b4Q1w+pGujkRiSfjQhMPuiERwGn4UMmQv+6UDE4qbPlnIN+e3e40JkvBhzgfzg==", - "dependencies": { - "JetBrains.Annotations": "2021.3.0" - } - }, "DotNext.Threading": { "type": "Direct", "requested": "[4.7.4, )", @@ -21,38 +12,11 @@ "System.Threading.Channels": "6.0.0" } }, - "fo-dicom": { - "type": "Direct", - "requested": "[5.0.3, )", - "resolved": "5.0.3", - "contentHash": "OPkCQ9+X/fvGRokAAgjR8bOpai04qlnNHmq+LsgI+Kyug3yar2zk6IMOSSvPOLgWe0EG9ScdqH44AGKnviH5Rw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "Microsoft.Extensions.DependencyInjection": "2.2.0", - "Microsoft.Extensions.Options": "2.2.0", - "Microsoft.Toolkit.HighPerformance": "7.1.2", - "System.Buffers": "4.5.1", - "System.Text.Encoding.CodePages": "4.6.0", - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.7.2", - "System.Threading.Channels": "6.0.0" - } - }, - "fo-dicom.NLog": { - "type": "Direct", - "requested": "[5.0.3, )", - "resolved": "5.0.3", - "contentHash": "k35FD+C9IcpTLjCF5tvCkBGUxJ+YvzoBsgb2VAtGQv+aVTu+HyoCnNVqccc4lVE53fbVCwpR3gPiTAnm5fm+KQ==", - "dependencies": { - "NLog": "4.7.11", - "fo-dicom": "5.0.3" - } - }, "HL7-dotnetcore": { "type": "Direct", - "requested": "[2.35.0, )", - "resolved": "2.35.0", - "contentHash": "1yScq0Ju2O/GPBasnr9/uHziKu3CBgh4nOkgJPWatPLTcP4EzUjjaM2hkgjOBMj8pKO0g687UDnj989MvYRLfA==" + "requested": "[2.36.0, )", + "resolved": "2.36.0", + "contentHash": "N1HLMeIqYuY+4O69ItgZJoDBnnpNkK5N2pClceTJ2nFJxsP48iCsA4iz3tm43Yszi4r/vaThoc3UoLBfGP3vKw==" }, "Karambolo.Extensions.Logging.File": { "type": "Direct", @@ -66,120 +30,15 @@ "System.Threading.Channels": "6.0.0" } }, - "Microsoft.EntityFrameworkCore": { - "type": "Direct", - "requested": "[6.0.15, )", - "resolved": "6.0.15", - "contentHash": "o51dv+X1Fv1/oPCWtCED4tTov4aBWD59ebkY5BW5K/8hwu+X+AfWpN1/bCBuS/3OPW24RuZmGfigByRMlG/fIA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "6.0.15", - "Microsoft.EntityFrameworkCore.Analyzers": "6.0.15", - "Microsoft.Extensions.Caching.Memory": "6.0.1", - "Microsoft.Extensions.DependencyInjection": "6.0.1", - "Microsoft.Extensions.Logging": "6.0.0", - "System.Collections.Immutable": "6.0.0", - "System.Diagnostics.DiagnosticSource": "6.0.1" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" - }, - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { - "type": "Direct", - "requested": "[6.0.15, )", - "resolved": "6.0.15", - "contentHash": "LmB5kbbc0Sr+XvnYj8tReZzubS50h1g463zpbnnjqT/k6fM8/od9hFCBj52dorXfp/DDfm5+rUdKaPRUsX70Jg==" - }, - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { - "type": "Direct", - "requested": "[6.0.15, )", - "resolved": "6.0.15", - "contentHash": "jIWboFkp6O/G3wF6JwQq8A5AR5TcZbCRzXdBhaYgVAGiWexb95/2JkytGFrJJ44pBiWO76jpOT4vShGLAgf1HQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "6.0.15", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.15", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15" - } - }, - "Microsoft.Extensions.Hosting": { - "type": "Direct", - "requested": "[6.0.1, )", - "resolved": "6.0.1", - "contentHash": "hbmizc9KPWOacLU8Z8YMaBG6KWdZFppczYV/KwnPGU/8xebWxQxdDeJmLOgg968prb7g2oQgnp6JVLX6lgby8g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Binder": "6.0.0", - "Microsoft.Extensions.Configuration.CommandLine": "6.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1", - "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.Configuration.UserSecrets": "6.0.1", - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Microsoft.Extensions.Logging.Console": "6.0.0", - "Microsoft.Extensions.Logging.Debug": "6.0.0", - "Microsoft.Extensions.Logging.EventLog": "6.0.0", - "Microsoft.Extensions.Logging.EventSource": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.DiagnosticSource": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.Console": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "gsqKzOEdsvq28QiXFxagmn1oRB9GeI5GgYCkoybZtQA0IUb7QPwf1WmN3AwJeNIsadTvIFQCiVK0OVIgKfOBGg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Configuration": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Text.Json": "6.0.0" - } - }, - "Microsoft.Extensions.Options": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" - } - }, "Monai.Deploy.Messaging.RabbitMQ": { "type": "Direct", - "requested": "[0.1.22, )", - "resolved": "0.1.22", - "contentHash": "ZJEHtM4NaX8UzvG+w1coKOivbCecoU6hx8g06PGKkg6giIeLGqCi2FDkP89kIPq7Kz1RB9cLVvYdXY9Rs+ZDSg==", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "1UiWYO+EjNZSFrL/SUElqmBo3TktG+XiCXm8oyXheEWz/CuZS2hhepYB4BDz7XAohUqt2/Hv7wpLiaauiaIFZg==", "dependencies": { - "Monai.Deploy.Messaging": "0.1.22", - "Polly": "7.2.3", - "RabbitMQ.Client": "6.4.0", - "System.Collections.Concurrent": "4.3.0" + "Monai.Deploy.Messaging": "1.0.0", + "Polly": "7.2.4", + "RabbitMQ.Client": "6.5.0" } }, "Monai.Deploy.Security": { @@ -197,57 +56,26 @@ "Microsoft.Extensions.Logging.Configuration": "6.0.0" } }, - "Monai.Deploy.Storage": { - "type": "Direct", - "requested": "[0.2.16, )", - "resolved": "0.2.16", - "contentHash": "UVu9yyiJf4mYlz1H6g29LXdYv5RRFcttudTczA3w3SuGQ5u3fEIRbDaWuTjFjaaLor+IuibGaob6c1L6ACH7kg==", - "dependencies": { - "AWSSDK.SecurityToken": "3.7.101.26", - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.Extensions.Configuration": "6.0.1", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.15", - "Microsoft.Extensions.Logging": "6.0.0", - "Monai.Deploy.Storage.S3Policy": "0.2.16", - "System.IO.Abstractions": "17.2.3" - } - }, "Monai.Deploy.Storage.MinIO": { "type": "Direct", - "requested": "[0.2.16, )", - "resolved": "0.2.16", - "contentHash": "/J8fylX91D/ePPCHTCgY11nPdADR5MZO239ZZWrK7ZO2ngAvKRmTB4Q6q6JbSErDF1hSk11HGhWCWNDsnK1rAQ==", + "requested": "[0.2.18, )", + "resolved": "0.2.18", + "contentHash": "0sHLiT0qU2Fg5O+AF8UDqzsJEYztUAFZeOPh4kOLC4bckhb+wSsuv7VcAXWtR3BOY6TxaMVVUJ+EK/o5mCp3tQ==", "dependencies": { - "AWSSDK.SecurityToken": "3.7.101.26", - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Minio": "4.0.7", - "Monai.Deploy.Storage": "0.2.16", - "Monai.Deploy.Storage.S3Policy": "0.2.16" + "Minio": "5.0.0", + "Monai.Deploy.Storage": "0.2.18", + "Monai.Deploy.Storage.S3Policy": "0.2.18" } }, - "NLog": { - "type": "Direct", - "requested": "[5.1.3, )", - "resolved": "5.1.3", - "contentHash": "rB8hwjBf1EZCfG5iPfsv3gPksLoJLr1cOrt7PBbJu6VpJgwYJchDzTUT1dhNDdPv0QakXJQJOhE59ErupcznQQ==" - }, "NLog.Web.AspNetCore": { "type": "Direct", - "requested": "[5.2.3, )", - "resolved": "5.2.3", - "contentHash": "uP0KekbkswuMjo1dbaqu20TxH2Dc3ox2qJDIi837ob2Fq/BliZHuQY9nJdM3UArVrLrsl+xxsx0D6h8m3fOufg==", + "requested": "[5.3.3, )", + "resolved": "5.3.3", + "contentHash": "ub8LOAbIGIPtv9nMAdZXlxUvszau6p2Svmeo8mhJFD+PQDMnI6PFc5IID1Jj3c1Lv8sxKVL7vRCsaWdTrmnrFw==", "dependencies": { - "NLog.Extensions.Logging": "5.2.3" + "NLog.Extensions.Logging": "5.3.3" } }, - "Polly": { - "type": "Direct", - "requested": "[7.2.3, )", - "resolved": "7.2.3", - "contentHash": "DeCY0OFbNdNxsjntr1gTXHJ5pKUwYzp04Er2LLeN3g6pWhffsGuKVfMBLe1lw7x76HrPkLxKEFxBlpRxS2nDEQ==" - }, "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[6.5.0, )", @@ -260,6 +88,11 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.5.0" } }, + "Ardalis.GuardClauses": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "+UcJ2s+gf2wMNrwadCaHZV2DMcGgBU1t22A+jm40P4MHQRLy9hcleGy5xdVWd4dXZPa5Vlp4TG5xU2rhoDYrBA==" + }, "AspNetCore.HealthChecks.MongoDb": { "type": "Transitive", "resolved": "6.0.2", @@ -271,24 +104,21 @@ }, "AWSSDK.Core": { "type": "Transitive", - "resolved": "3.7.105.20", - "contentHash": "ZHuTxP1J8g91+YSV0YLzm5te5lG+zkiUH/+NDHFpLf1cBD6iw2kUo5AkYEVxfEur1OTdYJxEZ5jDuOBE4pubkg==" + "resolved": "3.7.200.13", + "contentHash": "yiUuhTI8w183euRqhXym1DyhnD/1ccxceRoruCfkIoqY3PAaFgFL8pE4iDLDZa7SUW4M4qZnQ5PMlFr1rrl6zw==" }, "AWSSDK.SecurityToken": { "type": "Transitive", - "resolved": "3.7.101.26", - "contentHash": "/y64ogftqwGa07HNOj2Dh08oqYIgbIyfJFncneHy+fzC54VFhEIN5+pSOHS4Also1SSb9Erk/Knuf3L6jrTVEg==", + "resolved": "3.7.201.9", + "contentHash": "yKlTPrvNeDdzkOX82Ydf7MD09Gk3dK74JWZPRWJ3QIxskWVoNTAyLvfVBzbi+/fGnjf8/qKsSzxT7GHLqds37A==", "dependencies": { - "AWSSDK.Core": "[3.7.105.20, 4.0.0)" + "AWSSDK.Core": "[3.7.200.13, 4.0.0)" } }, - "Crc32.NET": { + "CommunityToolkit.HighPerformance": { "type": "Transitive", - "resolved": "1.2.0", - "contentHash": "wNW/huzolu8MNKUnwCVKxjfAlCFpeI8AZVfF46iAWJ1+P6bTU1AZct7VAkDDEjgeeTJCVTkGZaD6jSd/fOiUkA==", - "dependencies": { - "NETStandard.Library": "2.0.0" - } + "resolved": "8.2.0", + "contentHash": "iKzsPiSnXoQUN5AoApYmdfnLn9osNb+YCLWRr5PFmrDEQVIu7OeOyf4DPvNBvbqbYLZCfvHozPkulyv6zBQsFw==" }, "DnsClient": { "type": "Transitive", @@ -306,10 +136,23 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "JetBrains.Annotations": { + "fo-dicom": { "type": "Transitive", - "resolved": "2021.3.0", - "contentHash": "Ddxjs5RRjf+c8m9m++WvhW1lz1bqNhsTjWvCLbQN9bvKbkJeR9MhtfNwKgBRRdG2yLHcXFr5Lf7fsvvkiPaDRg==" + "resolved": "5.1.1", + "contentHash": "YraR81u9XuTN7l+pt6HzT0KvuhgWVZ9LCuHMH3zgFfAtv4peT1y+nYMSGwF9YqNP+oZnzh0s0PJ+vJMsTDpGIw==", + "dependencies": { + "CommunityToolkit.HighPerformance": "8.2.0", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Buffers": "4.5.1", + "System.Text.Encoding.CodePages": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.7", + "System.Threading.Channels": "6.0.0" + } }, "Macross.Json.Extensions": { "type": "Transitive", @@ -338,54 +181,73 @@ "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" + }, "Microsoft.CSharp": { "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + "resolved": "4.5.0", + "contentHash": "kaj6Wb4qoMuH3HySFJhxwQfe8R/sJsNJnANrvv8WdFPMoNbKY5htfNscv+LHCu5ipz+49m2e+WQXpLXr9XYemQ==" }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "yE5Q7jJDuGUwS3FMV6N6oz7p7MrtqPrdanLHG6dVXPB3o4KQKLpkPPzUQPByGmBis6wIDGmbWunwjD0vH/qlFQ==", + "resolved": "6.0.21", + "contentHash": "9S+kvYcPyGBqH5KX7sL0d7xYADTUrUVaBz+GZsSx4N4jKh/0mka6IFdeuFYzs3T6wdtHTvzdltcRwucwuTFpdw==", "dependencies": { "SQLitePCLRaw.core": "2.1.2" } }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "XUPcDrn/Vrv9yF4M3b9FYEZvqW1gyS3hfJhFiP0pttuRYnGRB+y3/6g/9k0GIoU62+XkxGa78l1JUccq1uvAXQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.21", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.21", + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Collections.Immutable": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.1" + } + }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "seE5q7/0R1LmWiQcd5pZYzlY8WdVojv2tk+5o0p4HrEvliOysomjIOYVEEHJnK9NwXqHBcZra4b+RwzgWYdbzA==" + "resolved": "6.0.21", + "contentHash": "GlNsy7qoFnCxgZlPpb8H/Srq1juOiV6W5XaijSA0+h8V0yn1VJ0owjb01If3di3Covs/8682A+ByTFjmEUxePA==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "0ZKFq5irkVVyPJmQDorRsWxXy85wKm+UPO8J6pf2h1ggGl1CkhlXa+bteM8NBo++Cfylv8cBSo8ZfQZHV57fIg==" + "resolved": "6.0.21", + "contentHash": "gS8tH419vOY2kEyqEZBX8VnXWmtHaor7gVx6zVaXCsEyQurGR/aVB++IZ62vzeQFS9R46LbNY6D6bqEA6j3iCg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "ouk4es/CzwxjXl33mb2hJzitluc2CD9rujZVBaUy3w3fn8qMjlktMOhf5mIAS7e3sreBikOBwaxp9/y/N/O2NQ==", + "resolved": "6.0.21", + "contentHash": "Ev5FM2KpXJu7+Dm9qvLf1FhSJMytrhSXho92Vompmgeiz3p4InldluidmKKmv8nZQAjs9dTCUUyvk1pxQjysaQ==", "dependencies": { - "Microsoft.EntityFrameworkCore": "6.0.15", + "Microsoft.EntityFrameworkCore": "6.0.21", "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "4oRXU58XmoDkK27wDMmIrZG9yaOYw8URmWNQzGkfO0ZCpELX/bx6rtb99eoBOOzA+a0QYoTLlugZB7MyM1XDbw==", + "resolved": "6.0.21", + "contentHash": "iAs1F5gxEQRRGNHDKJ6ZtoSbOAWcjdk+mABEIy2vRLeACp7xBPdQRQdJnENmxykkBgOVef73RpU3xVdDcn8Omg==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.15", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.21", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "30gMAP29sWQ9yTSM/VXknmv8BcH9AVO+QHCpoDoAlzPnmL6STjJ5jihlOp1mvErGVTkEgnaIxmv4j3gX6knFRw==", + "resolved": "6.0.21", + "contentHash": "2If1Lt04gD+KrKPFbMUeUzB8Av/EGJJFxNLGfC/CKLgy8+jAYsamyQ/Hux+93XCajJxFLnJimqSg+bBBvXX+2g==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "6.0.15", - "Microsoft.EntityFrameworkCore.Relational": "6.0.15", + "Microsoft.Data.Sqlite.Core": "6.0.21", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, @@ -439,24 +301,6 @@ "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Configuration.CommandLine": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "3nL1qCkZ1Oxx14ZTzgo4MmlO7tso7F+TtMZAY2jUAtTLyAcDp+EDjk3RqafoKiNaePyPvvlleEcBxh3b2Hzl1g==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "pnyXV1LFOsYjGveuC07xp0YHIyGq7jRq5Ncb5zrrIieMLWVwgMyYxcOH0jTnBedDT4Gh1QinSqsjqzcieHk1og==", - "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" - } - }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "6.0.0", @@ -481,17 +325,6 @@ "System.Text.Json": "6.0.0" } }, - "Microsoft.Extensions.Configuration.UserSecrets": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "Fy8yr4V6obi7ZxvKYI1i85jqtwMq8tqyxQVZpRSkgeA8enqy/KvBIMdcuNdznlxQMZa72mvbHqb7vbg4Pyx95w==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0" - } - }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "6.0.1", @@ -501,6 +334,11 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "6.0.0", @@ -515,15 +353,30 @@ }, "Microsoft.Extensions.Diagnostics.HealthChecks": { "type": "Transitive", - "resolved": "6.0.15", - "contentHash": "crR/15PKDgVIQmH9uGJuQVg4RGbaxwG3cseRRMisPG/2LkiQV71EkNRGPV4cI61Waywc1Wn5sYXE8bo2qCf+/Q==", + "resolved": "6.0.21", + "contentHash": "1Qf/tEg6IlzbvCxrc+pZE+ZGrajBdB/+V2+abeAu6lg8wXGHmO8JtnrNqwc5svSbcz3udxinUPyH3vw6ZujKbg==", "dependencies": { - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.3", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" } }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "5FSA1euCRtbRqVgTn2ahgCG9Cy29UQXAZMCJLUlrQQaC5rko0+d/aq9SiFGIDP7cvoWUsatrlNdfc6UyOMV5aA==" + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "6StjSICTiNdXK9NiQx0jpmsfJhSsXekjfJt8r/3K9qUx9dxVF8V2hhhIxRnZt8HM+4YagFLejNCD6hFUAnx9pw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -557,21 +410,22 @@ "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Http": { + "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "6.0.0", - "contentHash": "15+pa2G0bAMHbHewaQIdr/y6ag2H3yh4rd9hTXavtWDzQBkvpe2RMqFg8BxDpcQWssmjmBApGPcw93QRz6YcMg==", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "6.0.3", - "contentHash": "SUpStcdjeBbdKjPKe53hVVLkFjylX0yIXY8K+xWa47+o1d+REDyOMZjHZa+chsQI1K9qZeiHWk9jos0TFU7vGg==" + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", @@ -588,40 +442,13 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0" } }, - "Microsoft.Extensions.Logging.Debug": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "M9g/JixseSZATJE9tcMn9uzoD4+DbSglivFqVx8YkRJ7VVPmnvCEbOZ0AAaxsL1EKyI4cz07DXOOJExxNsUOHw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "rlo0RxlMd0WtLG3CHI0qOTp6fFn7MvQjlrCjucA31RqmiMFCZkF8CHNbe8O7tbBIyyoLGWB1he9CbaA5iyHthg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "Microsoft.Extensions.Logging.EventSource": { + "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "6.0.0", - "contentHash": "BeDyyqt7nkm/nr+Gdk+L8n1tUT/u33VkbXAOesgYSNsxDM9hJ1NOBGoZfj9rCbeD2+9myElI6JOVVFmnzgeWQA==", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Json": "6.0.0" + "Microsoft.Extensions.Primitives": "6.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { @@ -685,15 +512,6 @@ "System.Security.Cryptography.Cng": "4.5.0" } }, - "Microsoft.Net.Http.Headers": { - "type": "Transitive", - "resolved": "2.2.8", - "contentHash": "wHdwMv0QDDG2NWDSwax9cjkeQceGC1Qq53a31+31XpvTXVljKXRjWISlMoS/wZYKiqdqzuEvKFKwGHl+mt2jCA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "2.2.0", - "System.Buffers": "4.5.0" - } - }, "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "5.0.0", @@ -701,18 +519,23 @@ }, "Microsoft.NETCore.Targets": { "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "1.2.3", "contentHash": "Nug3rO+7Kl5/SBAadzSMAVgqDlfGjJZ0GenQrLywJ84XGKO0uRqkunz5Wyl0SDwcR71bAATXvSdbdzPrYRYKGw==" }, - "Microsoft.Toolkit.HighPerformance": { + "Microsoft.Win32.Primitives": { "type": "Transitive", - "resolved": "7.1.2", - "contentHash": "cezzRky0BUJyYmSrcQUcX8qAv90JfUwCqWEbqfWZLHyeANo9/LWgW6y50pqbyc8r8SPXVsu2GNH98fB3VxrnvA==" + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } }, "Microsoft.Win32.Registry": { "type": "Transitive", @@ -725,70 +548,75 @@ }, "Minio": { "type": "Transitive", - "resolved": "4.0.7", - "contentHash": "gmd+B4YHaE+cz8TCPIzkhov3t30VovGfyf6vMxqXd/iF/cziKmXKJJXv2E9kcHvbGwVHhMWtvk7fXWOzHcU4uw==", + "resolved": "5.0.0", + "contentHash": "7tZj90WEuuH60RAP4wBYexjMuJOhCnK7I46hCiX3CtZPackHisLZ8aAJmn3KlwbUX22dBDphwemD+h37vet8Qw==", "dependencies": { - "Crc32.NET": "1.2.0", - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.1", - "System.Net.Http": "4.3.4", - "System.Net.Primitives": "4.3.1", - "System.Reactive.Linq": "5.0.0", - "System.ValueTuple": "4.4.0" + "CommunityToolkit.HighPerformance": "8.1.0", + "System.IO.Hashing": "7.0.0", + "System.Reactive.Linq": "5.0.0" } }, "Monai.Deploy.Messaging": { "type": "Transitive", - "resolved": "0.1.22", - "contentHash": "pFZBuV3TaZvZJz8wTib8G/Doa/XHkM8uv12VtuLkQc7lI8AbJmH1eIHnpRliyuKPmw7VMhOMiS7JhyqutC0uvQ==", + "resolved": "1.0.0", + "contentHash": "Xr1V3ZrSJByfUP4w+aiOAqC7Uzt1GqRXj35qSTQs9C1oI4gCiBN4wnre0SSvoA7vHQNZPGWNWXtiqbI7Cov3Mg==", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", - "Microsoft.Extensions.Configuration": "6.0.1", - "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.14", - "Microsoft.Extensions.Logging": "6.0.0", + "Ardalis.GuardClauses": "4.1.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", "Newtonsoft.Json": "13.0.3", - "System.ComponentModel.Annotations": "5.0.0", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+1JX7QDgVEMqYA0/M1QMr1gtXRC6lEuhBtLfJXWi6cEgh9kOPE0KiHd1AWI7PxBgBbsEBZaNQSvWqShlwcu6bA==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.201.9", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Monai.Deploy.Storage.S3Policy": "0.2.18", "System.IO.Abstractions": "17.2.3" } }, "Monai.Deploy.Storage.S3Policy": { "type": "Transitive", - "resolved": "0.2.16", - "contentHash": "KfiAW28llXSlDLgGwAeBCZGns9/5D/wHVOKt2NxzeiJpp0fR3VspQIbuHO1cbn4GcDM7tL0Se4gR494rHGoz4g==", + "resolved": "0.2.18", + "contentHash": "+b0nDnf4OoajdH2hB02elRC6G+GjlYnxJC+F3dGbUUXGMtPApzs8c8s/EG4fKzihxsVovJtqnJl7atcaPyl12Q==", "dependencies": { - "Ardalis.GuardClauses": "4.0.1", + "Ardalis.GuardClauses": "4.1.1", "Newtonsoft.Json": "13.0.3" } }, "MongoDB.Bson": { "type": "Transitive", - "resolved": "2.19.1", - "contentHash": "4FSR3eAbJEYMmvQ1pNFImUpFGtGHT+kEw/Yw/KZjxC9iFMj1XcZC08wMbezgRga2F9tNNFG2vDqh9zt01GinMA==", + "resolved": "2.21.0", + "contentHash": "QT+D1I3Jz6r6S6kCgJD1L9dRCLVJCKlkGRkA+tJ7uLpHRmjDNcNKy4D1T+L9gQrjl95lDN9PHdwEytdvCW/jzA==", "dependencies": { + "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "5.0.0" } }, "MongoDB.Driver": { "type": "Transitive", - "resolved": "2.19.1", - "contentHash": "EeQnUCIzRmXg20jwHSM9uvw67nrEMpINKsJDF9Y8xFh/8WFWD9QjZyyJLZgUoFUSz9pUAbyLfQj+ctJYbn8gxg==", + "resolved": "2.21.0", + "contentHash": "VxKj1wuhadiXhaXkykCWRgsYOysdaOYJ202hJFz25UjkrqC/tHA8RS4hdS5HYfGWoI//fypBXnxZCkEjXLXdfw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "2.0.0", - "MongoDB.Bson": "2.19.1", - "MongoDB.Driver.Core": "2.19.1", - "MongoDB.Libmongocrypt": "1.7.0" + "MongoDB.Bson": "2.21.0", + "MongoDB.Driver.Core": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0" } }, "MongoDB.Driver.Core": { "type": "Transitive", - "resolved": "2.19.1", - "contentHash": "+T4+vNZHCjp7qoOoNE8hf8VjnwxZttTOHTqv0jibJ4WSnM6lnXZBP4wBOjIKDF3J4aQffvtaZtIt4UWDOV+yAw==", + "resolved": "2.21.0", + "contentHash": "Ac44U3bQfinmdH5KNFjTidJe9LKW87SxkXJ3YuIUJQMITEc4083YF1yvjJxaSeYF9er0YgHSmwhHpsZv0Fwplg==", "dependencies": { "AWSSDK.SecurityToken": "3.7.100.14", "DnsClient": "1.6.1", "Microsoft.Extensions.Logging.Abstractions": "2.0.0", - "MongoDB.Bson": "2.19.1", - "MongoDB.Libmongocrypt": "1.7.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0", "SharpCompress": "0.30.1", "Snappier": "1.0.0", "System.Buffers": "4.5.1", @@ -797,15 +625,58 @@ }, "MongoDB.Libmongocrypt": { "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "p9+peTZX63nGHskOLhvhfBtrknxNg1RzXepE07rPozuCGz27bMjCcQyvn2YByg0L3YEcNWdTmI4BlnG/5RF+5Q==" + "resolved": "1.8.0", + "contentHash": "fgNw8Dxpkq7mpoaAYes8cfnPRzvFIoB8oL9GPXwi3op/rONftl0WAeg4akRLcxfoVuUvuUO2wGoVBr3JzJ7Svw==" }, "NETStandard.Library": { "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" } }, "Newtonsoft.Json": { @@ -822,39 +693,49 @@ "Newtonsoft.Json": "10.0.1" } }, - "NLog.Extensions.Logging": { + "NLog": { "type": "Transitive", "resolved": "5.2.3", - "contentHash": "TB8zPGV2nVpvWq5C8zIVHPSmnzOHMrXppjsAwHcuJq1Ehs8sC0llnAv5Ysf5Lf/vew9amV/+01MohtRFSDzKdQ==", + "contentHash": "rHTNRtQF5qYqLutSR9ldUWXglKym/KA1R6GKw4JtDvza8i5+kgfmeKH75Ccn1noeJIOjHLXorphMxKk3EiN2tg==" + }, + "NLog.Extensions.Logging": { + "type": "Transitive", + "resolved": "5.3.3", + "contentHash": "o3V1oUr0izjhU1djuVqN5JdmNUGmunTs3Amjhumt/nxva8kG9QWjOdba+ciwkP//QOjv+KkGklZtI9o4qz50hQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", "Microsoft.Extensions.Logging": "6.0.0", - "NLog": "5.1.3" + "NLog": "5.2.3" } }, + "Polly": { + "type": "Transitive", + "resolved": "7.2.4", + "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA==" + }, "RabbitMQ.Client": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "1znR1gGU+xYVSpO5z8nQolcUKA/yydnxQn7Ug9+RUXxTSLMm/eE58VKGwahPBjELXvDnX0k/kBrAitFLRjx9LA==", + "resolved": "6.5.0", + "contentHash": "9hY5HiWPtCla1/l0WmXmLnqoX7iKE3neBQUWnetIJrRpOvTbO//XQfQDh++xgHCshL40Kv/6bR0HDkmJz46twg==", "dependencies": { - "System.Memory": "4.5.4", - "System.Threading.Channels": "4.7.1" + "System.Memory": "4.5.5", + "System.Threading.Channels": "7.0.0" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" }, "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" }, "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" }, "runtime.native.System": { "type": "Transitive", @@ -865,6 +746,15 @@ "Microsoft.NETCore.Targets": "1.1.0" } }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, "runtime.native.System.Net.Http": { "type": "Transitive", "resolved": "4.3.0", @@ -884,30 +774,30 @@ }, "runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" }, "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" }, "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { "type": "Transitive", @@ -916,28 +806,28 @@ }, "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" }, "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" }, "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" }, "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" }, "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" }, "SharpCompress": { "type": "Transitive", @@ -1000,6 +890,14 @@ "resolved": "6.5.0", "contentHash": "OvbvxX+wL8skxTBttcBsVxdh73Fag4xwqEU2edh4JMn7Ws/xJHnY/JB1e9RoCb6XpDxUF3hD9A0Z1lEUx40Pfw==" }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Buffers": { "type": "Transitive", "resolved": "4.5.1", @@ -1040,10 +938,17 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "System.ComponentModel.Annotations": { + "System.Console": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } }, "System.Diagnostics.Debug": { "type": "Transitive", @@ -1063,10 +968,15 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "System.Diagnostics.EventLog": { + "System.Diagnostics.Tools": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } }, "System.Diagnostics.Tracing": { "type": "Transitive", @@ -1138,6 +1048,44 @@ "resolved": "17.2.3", "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.IO.FileSystem": { "type": "Transitive", "resolved": "4.3.0", @@ -1161,6 +1109,11 @@ "System.Runtime": "4.3.0" } }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "sDnWM0N3AMCa86LrKTWeF3BZLD2sgWyYUc7HL6z4+xyDZNQRwzmxbo4qP2rX2MqC+Sy1/gOSRDah5ltxY5jPxw==" + }, "System.Linq": { "type": "Transitive", "resolved": "4.3.0", @@ -1181,17 +1134,41 @@ "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "System.Net.Http": { "type": "Transitive", - "resolved": "4.3.4", - "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Platforms": "1.1.0", "System.Collections": "4.3.0", "System.Diagnostics.Debug": "4.3.0", "System.Diagnostics.DiagnosticSource": "4.3.0", @@ -1216,20 +1193,45 @@ "System.Threading.Tasks": "4.3.0", "runtime.native.System": "4.3.0", "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, "System.Net.Primitives": { "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "OHzPhSme78BbmLe9UBxHM69ZYjClS5URuhce6Ta4ikiLgaUGiG/X84fZpI6zy7CsUH5R9cYzI2tv9dWPqdTkUg==", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "System.Runtime": "4.3.1", + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", "System.Runtime.Handles": "4.3.0" } }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Reactive": { "type": "Transitive", "resolved": "5.0.0", @@ -1256,6 +1258,50 @@ "System.Runtime": "4.3.0" } }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Reflection.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -1266,6 +1312,15 @@ "System.Runtime": "4.3.0" } }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Resources.ResourceManager": { "type": "Transitive", "resolved": "4.3.0", @@ -1280,11 +1335,11 @@ }, "System.Runtime": { "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" } }, "System.Runtime.CompilerServices.Unsafe": { @@ -1325,6 +1380,20 @@ "System.Runtime.Handles": "4.3.0" } }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, "System.Runtime.Numerics": { "type": "Transitive", "resolved": "4.3.0", @@ -1493,10 +1562,21 @@ }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "OCUK9C/U97+UheVwo+JE+IUcKySUE3Oe+BcHhVtQrvmKSUFLrUDO8B5zEPRL6mBGbczxZp4w1boSck6/fw4dog==", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", "dependencies": { - "Microsoft.NETCore.Platforms": "3.0.0" + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" } }, "System.Text.Encodings.Web": { @@ -1516,6 +1596,14 @@ "System.Text.Encodings.Web": "6.0.0" } }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Threading": { "type": "Transitive", "resolved": "4.3.0", @@ -1527,8 +1615,8 @@ }, "System.Threading.Channels": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" }, "System.Threading.Tasks": { "type": "Transitive", @@ -1540,20 +1628,61 @@ "System.Runtime": "4.3.0" } }, - "System.Threading.Tasks.Dataflow": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA==" - }, "System.Threading.Tasks.Extensions": { "type": "Transitive", "resolved": "4.5.4", "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" }, - "System.ValueTuple": { + "System.Threading.Timer": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "BahUww/+mdP4ARCAh2RQhQTg13wYLVrBb9SYVgW8ZlrwjraGCXHGjo0oIiUfZ34LUZkMMR+RAzR7dEY4S1HeQQ==" + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } }, "ZstdSharp.Port": { "type": "Transitive", @@ -1564,49 +1693,38 @@ "type": "Project", "dependencies": { "Macross.Json.Extensions": "[3.0.0, )", - "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.15, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.21, )", "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[0.1.22, )", - "Monai.Deploy.Storage": "[0.2.16, )" + "Monai.Deploy.Messaging": "[1.0.0, )", + "Monai.Deploy.Storage": "[0.2.18, )", + "fo-dicom": "[5.1.1, )" } }, "monai.deploy.informaticsgateway.client.common": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "[4.0.1, )", - "System.Text.Json": "[6.0.7, )" + "Ardalis.GuardClauses": "[4.1.1, )" } }, "monai.deploy.informaticsgateway.common": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "[4.0.1, )", - "System.IO.Abstractions": "[17.2.3, )", - "System.Threading.Tasks.Dataflow": "[6.0.0, )", - "fo-dicom": "[5.0.3, )" + "Ardalis.GuardClauses": "[4.1.1, )", + "System.IO.Abstractions": "[17.2.3, )" } }, "monai.deploy.informaticsgateway.configuration": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[6.0.3, )", - "Microsoft.Extensions.Options": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", - "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", - "Monai.Deploy.Messaging": "[0.1.22, )", - "Monai.Deploy.Storage": "[0.2.16, )", - "System.IO.Abstractions": "[17.2.3, )" + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )" } }, "monai.deploy.informaticsgateway.database": { "type": "Project", "dependencies": { "AspNetCore.HealthChecks.MongoDb": "[6.0.2, )", - "Microsoft.EntityFrameworkCore": "[6.0.15, )", - "Microsoft.Extensions.Configuration": "[6.0.1, )", - "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", - "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.15, )", + "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore": "[6.0.21, )", "Microsoft.Extensions.Options.ConfigurationExtensions": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", @@ -1618,43 +1736,38 @@ "monai.deploy.informaticsgateway.database.api": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.15, )", "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", - "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", - "Polly": "[7.2.3, )" + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )" } }, "monai.deploy.informaticsgateway.database.entityframework": { "type": "Project", "dependencies": { - "Microsoft.EntityFrameworkCore": "[6.0.15, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.15, )", - "Microsoft.Extensions.Configuration": "[6.0.1, )", + "Microsoft.EntityFrameworkCore": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.21, )", "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", - "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )" + "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", + "Polly": "[7.2.4, )" } }, "monai.deploy.informaticsgateway.database.mongodb": { "type": "Project", "dependencies": { "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", - "MongoDB.Driver": "[2.19.1, )", - "MongoDB.Driver.Core": "[2.19.1, )" + "MongoDB.Driver": "[2.21.0, )", + "Polly": "[7.2.4, )" } }, "monai.deploy.informaticsgateway.dicomweb.client": { "type": "Project", "dependencies": { - "Ardalis.GuardClauses": "[4.0.1, )", "Microsoft.AspNet.WebApi.Client": "[5.2.9, )", - "Microsoft.Extensions.Http": "[6.0.0, )", - "Microsoft.Net.Http.Headers": "[2.2.8, )", "Monai.Deploy.InformaticsGateway.Client.Common": "[1.0.0, )", "System.Linq.Async": "[6.0.1, )", - "fo-dicom": "[5.0.3, )" + "fo-dicom": "[5.1.1, )" } } } diff --git a/src/Monai.Deploy.InformaticsGateway.sln b/src/Monai.Deploy.InformaticsGateway.sln index 1bf686dba..d851e6255 100644 --- a/src/Monai.Deploy.InformaticsGateway.sln +++ b/src/Monai.Deploy.InformaticsGateway.sln @@ -54,7 +54,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.Database.MongoDB", "Database\MongoDB\Monai.Deploy.InformaticsGateway.Database.MongoDB.csproj", "{5ED73EEA-4DFA-426D-82E8-AA24D3CB4C31}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test", "Database\MongoDB\Integration.Test\Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test.csproj", "{2F849556-44B6-484A-B612-CB0FA5D29AC6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test", "Database\MongoDB\Integration.Test\Monai.Deploy.InformaticsGateway.Database.MongoDB.Integration.Test.csproj", "{2F849556-44B6-484A-B612-CB0FA5D29AC6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution", "Plug-ins\RemoteAppExecution\Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj", "{8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test", "Plug-ins\RemoteAppExecution\Test\Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj", "{8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.InformaticsGateway.Test.PlugIns", "InformaticsGateway\Test\Plug-ins\Monai.Deploy.InformaticsGateway.Test.PlugIns.csproj", "{6C83469B-4B8A-416E-ACA7-09454D721352}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -366,6 +372,42 @@ Global {2F849556-44B6-484A-B612-CB0FA5D29AC6}.Release|x64.Build.0 = Release|Any CPU {2F849556-44B6-484A-B612-CB0FA5D29AC6}.Release|x86.ActiveCfg = Release|Any CPU {2F849556-44B6-484A-B612-CB0FA5D29AC6}.Release|x86.Build.0 = Release|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Debug|x64.Build.0 = Debug|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Debug|x86.Build.0 = Debug|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Release|Any CPU.Build.0 = Release|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Release|x64.ActiveCfg = Release|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Release|x64.Build.0 = Release|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Release|x86.ActiveCfg = Release|Any CPU + {8D82BD38-A21D-4C16-9FB0-616B9E7BFF23}.Release|x86.Build.0 = Release|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Debug|x64.ActiveCfg = Debug|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Debug|x64.Build.0 = Debug|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Debug|x86.ActiveCfg = Debug|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Debug|x86.Build.0 = Debug|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Release|Any CPU.Build.0 = Release|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Release|x64.ActiveCfg = Release|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Release|x64.Build.0 = Release|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Release|x86.ActiveCfg = Release|Any CPU + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177}.Release|x86.Build.0 = Release|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Debug|x64.Build.0 = Debug|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Debug|x86.Build.0 = Debug|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Release|Any CPU.Build.0 = Release|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Release|x64.ActiveCfg = Release|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Release|x64.Build.0 = Release|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Release|x86.ActiveCfg = Release|Any CPU + {6C83469B-4B8A-416E-ACA7-09454D721352}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -387,6 +429,8 @@ Global {EA930DE2-33C4-447C-9E26-31387652D408} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86} {5ED73EEA-4DFA-426D-82E8-AA24D3CB4C31} = {290E4C9B-841D-4E2C-91A0-5A69BAB122F3} {2F849556-44B6-484A-B612-CB0FA5D29AC6} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86} + {8EDD99D5-9FB5-40A3-BE47-B083D6DBC177} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86} + {6C83469B-4B8A-416E-ACA7-09454D721352} = {B8E99EF7-84EA-4D11-B722-9EE81B89CD86} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E23DC856-D033-49F6-9BC6-9F1D0ECD05CB} diff --git a/src/Plug-ins/RemoteAppExecution/Database/DatabaseRegistrar.cs b/src/Plug-ins/RemoteAppExecution/Database/DatabaseRegistrar.cs new file mode 100644 index 000000000..62e49ba8b --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/DatabaseRegistrar.cs @@ -0,0 +1,50 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.InformaticsGateway.Database.Api; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database +{ + public class DatabaseRegistrar : DatabaseRegistrationBase + { + public override IServiceCollection Configure(IServiceCollection services, DatabaseType databaseType, string? connectionString) + { + Guard.Against.Null(services, nameof(services)); + + switch (databaseType) + { + case DatabaseType.EntityFramework: + Guard.Against.Null(connectionString, nameof(connectionString)); + services.AddDbContext(options => options.UseSqlite(connectionString), ServiceLifetime.Transient); + services.AddScoped(); + + services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(EntityFramework.RemoteAppExecutionRepository)); + break; + + case DatabaseType.MongoDb: + services.AddScoped(); + + services.AddScoped(typeof(IRemoteAppExecutionRepository), typeof(MongoDb.RemoteAppExecutionRepository)); + break; + } + + return services; + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/MigrationManager.cs b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/MigrationManager.cs new file mode 100644 index 000000000..a8116c360 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/MigrationManager.cs @@ -0,0 +1,48 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Database.Api; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework +{ + public class MigrationManager : IDatabaseMigrationManagerForPlugIns + { + public IHost Migrate(IHost host) + { + using (var scope = host.Services.CreateScope()) + { + using (var dbContext = scope.ServiceProvider.GetRequiredService()) + { + try + { + dbContext.Database.Migrate(); + } + catch (Exception ex) + { + var logger = scope.ServiceProvider.GetService(); + logger?.Log(LogLevel.Critical, "Failed to migrate database", ex); + throw; + } + } + } + return host; + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionConfiguration.cs b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionConfiguration.cs new file mode 100644 index 000000000..63973bb87 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionConfiguration.cs @@ -0,0 +1,65 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework +{ +#pragma warning disable CS8604, CS8603 + + internal class RemoteAppExecutionConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + var dictValueComparer = new ValueComparer>( + (c1, c2) => c1!.Equals(c2), + c => c.GetHashCode(), + c => c.ToDictionary(entry => entry.Key, entry => entry.Value)); + + var jsonSerializerSettings = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + builder.HasKey(j => j.Id); + + builder.Property(j => j.WorkflowInstanceId).IsRequired(); + builder.Property(j => j.ExportTaskId).IsRequired(); + builder.Property(j => j.CorrelationId).IsRequired(); + builder.Property(j => j.RequestTime).IsRequired(); + + builder.Property(j => j.StudyInstanceUid).IsRequired(); + builder.Property(j => j.SeriesInstanceUid).IsRequired(); + builder.Property(j => j.SopInstanceUid).IsRequired(); + + builder.Property(j => j.OriginalValues) + .HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerSettings), + v => JsonSerializer.Deserialize>(v, jsonSerializerSettings)) + .Metadata.SetValueComparer(dictValueComparer); + + builder.HasIndex(p => new { p.WorkflowInstanceId, p.ExportTaskId, p.StudyInstanceUid, p.SeriesInstanceUid }, "idx_remoteapp_all"); + builder.HasIndex(p => new { p.WorkflowInstanceId, p.ExportTaskId, p.StudyInstanceUid }, "idx_remoteapp_study"); + builder.HasIndex(p => p.SopInstanceUid, "idx_remoteapp_instance"); + } + } + +#pragma warning restore CS8604, CS8603 +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContext.cs b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContext.cs new file mode 100644 index 000000000..4e1755edf --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContext.cs @@ -0,0 +1,70 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework +{ +#pragma warning disable CS8618 // Unread "private" fields should be removed + + public class RemoteAppExecutionDbContext : DbContext + { + public RemoteAppExecutionDbContext(DbContextOptions options) : base(options) + { + } + + public virtual DbSet RemoteAppExecutions { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ApplyConfiguration(new RemoteAppExecutionConfiguration()); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.ConfigureWarnings(c => c.Log( + (CoreEventId.SaveChangesCompleted, LogLevel.Trace), + (CoreEventId.SaveChangesStarting, LogLevel.Trace), + (CoreEventId.DetectChangesStarting, LogLevel.Trace), + (CoreEventId.DetectChangesCompleted, LogLevel.Trace), + (CoreEventId.StartedTracking, LogLevel.Trace), + (CoreEventId.ContextInitialized, LogLevel.Trace), + (CoreEventId.StateChanged, LogLevel.Trace), + (CoreEventId.QueryCompilationStarting, LogLevel.Trace), + (CoreEventId.QueryExecutionPlanned, LogLevel.Trace), + (RelationalEventId.CommandExecuting, LogLevel.Trace), + (RelationalEventId.CommandExecuted, LogLevel.Trace), + (RelationalEventId.ConnectionClosing, LogLevel.Trace), + (RelationalEventId.ConnectionClosed, LogLevel.Trace), + (RelationalEventId.DataReaderDisposing, LogLevel.Trace), + (RelationalEventId.ConnectionOpening, LogLevel.Trace), + (RelationalEventId.ConnectionOpened, LogLevel.Trace), + (RelationalEventId.CommandCreating, LogLevel.Trace), + (RelationalEventId.CommandCreating, LogLevel.Trace), + (RelationalEventId.TransactionStarted, LogLevel.Trace), + (RelationalEventId.TransactionStarting, LogLevel.Trace), + (RelationalEventId.TransactionCommitted, LogLevel.Trace), + (RelationalEventId.TransactionCommitting, LogLevel.Trace), + (RelationalEventId.TransactionDisposed, LogLevel.Trace), + (RelationalEventId.CommandCreated, LogLevel.Trace) + )); + } + +#pragma warning restore CS8618 // Unread "private" fields should be removed +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContextFactory.cs b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContextFactory.cs new file mode 100644 index 000000000..897323d3f --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionDbContextFactory.cs @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework +{ + /// + /// Used to EF migration. + /// + public class RemoteAppExecutionDbContextFactory : IDesignTimeDbContextFactory + { + public RemoteAppExecutionDbContext CreateDbContext(string[] args) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + var builder = new DbContextOptionsBuilder(); + + var connectionString = configuration.GetConnectionString(InformaticsGateway.Database.Api.SR.DatabaseConnectionStringKey); + builder.UseSqlite(connectionString); + + return new RemoteAppExecutionDbContext(builder.Options); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionRepository.cs b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionRepository.cs new file mode 100644 index 000000000..6f34c5923 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/EntityFramework/RemoteAppExecutionRepository.cs @@ -0,0 +1,139 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ardalis.GuardClauses; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework +{ + public class RemoteAppExecutionRepository : IRemoteAppExecutionRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly RemoteAppExecutionDbContext _dbContext; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly DbSet _dataset; + private bool _disposedValue; + + // Note. this implementation (unlike the Mongo one) Does not delete the entries + // so a cleanup routine will have to be implemented to peridoically remove old entries ! + + public RemoteAppExecutionRepository( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _dbContext = _scope.ServiceProvider.GetRequiredService(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Database.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + _dataset = _dbContext.Set(); + } + + public async Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default) + { + Guard.Against.Null(item, nameof(item)); + + return await _retryPolicy.ExecuteAsync(async () => + { + await _dataset.AddAsync(item, cancellationToken).ConfigureAwait(false); + await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return true; + }).ConfigureAwait(false); + } + + public async Task RemoveAsync(RemoteAppExecution remoteAppExecution, CancellationToken cancellationToken = default) + { + Guard.Against.Null(remoteAppExecution, nameof(remoteAppExecution)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = _dataset.Remove(remoteAppExecution); + await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return result.Entity; + }).ConfigureAwait(false); + } + + public async Task GetAsync(string sopInstanceUid, CancellationToken cancellationToken = default) + { + Guard.Against.NullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid)); + + return await _retryPolicy.ExecuteAsync(async () => + { + return await _dataset.SingleOrDefaultAsync(p => + p.SopInstanceUid.Equals(sopInstanceUid)).ConfigureAwait(false); + }).ConfigureAwait(false); + } + + public async Task GetAsync(string workflowInstanceId, string exportTaskId, string studyInstanceUid, string seriesInstanceUid, CancellationToken cancellationToken = default) + { + Guard.Against.NullOrWhiteSpace(workflowInstanceId, nameof(workflowInstanceId)); + Guard.Against.NullOrWhiteSpace(exportTaskId, nameof(exportTaskId)); + Guard.Against.NullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); + Guard.Against.NullOrWhiteSpace(seriesInstanceUid, nameof(seriesInstanceUid)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _dataset.SingleOrDefaultAsync(p => + p.WorkflowInstanceId.Equals(workflowInstanceId) && + p.ExportTaskId.Equals(exportTaskId) && + p.StudyInstanceUid.Equals(studyInstanceUid) && + p.SeriesInstanceUid.Equals(seriesInstanceUid)).ConfigureAwait(false); + + result ??= await _dataset.SingleOrDefaultAsync(p => + p.WorkflowInstanceId.Equals(workflowInstanceId) && + p.ExportTaskId.Equals(exportTaskId) && + p.StudyInstanceUid.Equals(studyInstanceUid)).ConfigureAwait(false); + + return result; + }).ConfigureAwait(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _dbContext.Dispose(); + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/IRemoteAppExecutionRepository.cs b/src/Plug-ins/RemoteAppExecution/Database/IRemoteAppExecutionRepository.cs new file mode 100644 index 000000000..8a262053e --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/IRemoteAppExecutionRepository.cs @@ -0,0 +1,29 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database +{ + public interface IRemoteAppExecutionRepository + { + Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default); + + Task GetAsync(string sopInstanceUid, CancellationToken cancellationToken = default); + + Task GetAsync(string workflowInstanceId, string exportTaskId, string studyInstanceUid, string seriesInstanceUid, CancellationToken cancellationToken = default); + + Task RemoveAsync(RemoteAppExecution remoteAppExecution, CancellationToken cancellationToken = default); + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/MongoDb/MigrationManager.cs b/src/Plug-ins/RemoteAppExecution/Database/MongoDb/MigrationManager.cs new file mode 100644 index 000000000..4d639fcf9 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/MongoDb/MigrationManager.cs @@ -0,0 +1,30 @@ +/* + * Copyright 2022-2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.Hosting; +using Monai.Deploy.InformaticsGateway.Database.Api; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb +{ + public class MigrationManager : IDatabaseMigrationManagerForPlugIns + { + public IHost Migrate(IHost host) + { + RemoteAppExecutionConfiguration.Configure(); + return host; + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionConfiguration.cs b/src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionConfiguration.cs new file mode 100644 index 000000000..56641afe3 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionConfiguration.cs @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * Copyright 2021 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson.Serialization; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb +{ + internal static class RemoteAppExecutionConfiguration + { + public static void Configure() + { + BsonClassMap.RegisterClassMap(j => + { + j.AutoMap(); + j.SetIdMember(j.GetMemberMap(c => c.Id)); + j.MapIdMember(c => c.Id); + j.SetIgnoreExtraElements(true); + }); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionRepository.cs b/src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionRepository.cs new file mode 100644 index 000000000..41aec37b8 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Database/MongoDb/RemoteAppExecutionRepository.cs @@ -0,0 +1,167 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ardalis.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.Database.Api.Logging; +using MongoDB.Driver; +using Polly; +using Polly.Retry; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb +{ + public class RemoteAppExecutionRepository : IRemoteAppExecutionRepository, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScope _scope; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly IMongoCollection _collection; + private bool _disposedValue; + + public RemoteAppExecutionRepository(IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options, + IOptions mongoDbOptions) + { + Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); + Guard.Against.Null(options, nameof(options)); + Guard.Against.Null(mongoDbOptions, nameof(mongoDbOptions)); + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _scope = serviceScopeFactory.CreateScope(); + _retryPolicy = Policy.Handle().WaitAndRetryAsync( + options.Value.Database.Retries.RetryDelays, + (exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception)); + + var mongoDbClient = _scope.ServiceProvider.GetRequiredService(); + var mongoDatabase = mongoDbClient.GetDatabase(mongoDbOptions.Value.DatabaseName); + _collection = mongoDatabase.GetCollection(nameof(RemoteAppExecution)); + CreateIndexes(); + } + + private void CreateIndexes() + { + var options = new CreateIndexOptions { Unique = true }; + var indexDefinitionState = Builders.IndexKeys.Ascending(_ => _.SopInstanceUid); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionState, options)); + + var indexDefinitionSeriesLevel = Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(_ => _.WorkflowInstanceId), + Builders.IndexKeys.Ascending(_ => _.ExportTaskId), + Builders.IndexKeys.Ascending(_ => _.StudyInstanceUid), + Builders.IndexKeys.Ascending(_ => _.SeriesInstanceUid)); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionSeriesLevel, options)); + + var indexDefinitionStudyLevel = Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(_ => _.WorkflowInstanceId), + Builders.IndexKeys.Ascending(_ => _.ExportTaskId), + Builders.IndexKeys.Ascending(_ => _.StudyInstanceUid)); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionStudyLevel, options)); + + options = new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(7), Name = "RequestTime" }; + indexDefinitionState = Builders.IndexKeys.Ascending(_ => _.RequestTime); + _collection.Indexes.CreateOne(new CreateIndexModel(indexDefinitionState, options)); + } + + public async Task AddAsync(RemoteAppExecution item, CancellationToken cancellationToken = default) + { + Guard.Against.Null(item, nameof(item)); + + return await _retryPolicy.ExecuteAsync(async () => + { + await _collection.InsertOneAsync(item, cancellationToken: cancellationToken).ConfigureAwait(false); + return true; + }).ConfigureAwait(false); + } + + public async Task RemoveAsync(RemoteAppExecution remoteAppExecution, CancellationToken cancellationToken = default) + { + Guard.Against.Null(remoteAppExecution, nameof(remoteAppExecution)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection.DeleteOneAsync(Builders.Filter.Where(p => p.Id == remoteAppExecution.Id), cancellationToken: cancellationToken).ConfigureAwait(false); + if (result.DeletedCount == 0) + { + throw new DatabaseException("Failed to delete entity"); + } + return remoteAppExecution; + }).ConfigureAwait(false); + } + + public async Task GetAsync(string sopInstanceUid, CancellationToken cancellationToken = default) + { + Guard.Against.NullOrWhiteSpace(sopInstanceUid, nameof(sopInstanceUid)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection.Find(p => + p.SopInstanceUid.Equals(sopInstanceUid, StringComparison.OrdinalIgnoreCase)).FirstOrDefaultAsync().ConfigureAwait(false); + + return result; + }).ConfigureAwait(false); + } + + public async Task GetAsync(string workflowInstanceId, string exportTaskId, string studyInstanceUid, string seriesInstanceUid, CancellationToken cancellationToken = default) + { + Guard.Against.NullOrWhiteSpace(workflowInstanceId, nameof(workflowInstanceId)); + Guard.Against.NullOrWhiteSpace(exportTaskId, nameof(exportTaskId)); + Guard.Against.NullOrWhiteSpace(studyInstanceUid, nameof(studyInstanceUid)); + Guard.Against.NullOrWhiteSpace(seriesInstanceUid, nameof(seriesInstanceUid)); + + return await _retryPolicy.ExecuteAsync(async () => + { + var result = await _collection.Find(p => + p.WorkflowInstanceId.Equals(workflowInstanceId, StringComparison.OrdinalIgnoreCase) && + p.ExportTaskId.Equals(exportTaskId, StringComparison.OrdinalIgnoreCase) && + p.StudyInstanceUid.Equals(studyInstanceUid, StringComparison.OrdinalIgnoreCase) && + p.SeriesInstanceUid.Equals(seriesInstanceUid, StringComparison.OrdinalIgnoreCase)).FirstOrDefaultAsync().ConfigureAwait(false); + + result ??= await _collection.Find(p => + p.WorkflowInstanceId.Equals(workflowInstanceId, StringComparison.OrdinalIgnoreCase) && + p.ExportTaskId.Equals(exportTaskId, StringComparison.OrdinalIgnoreCase) && + p.StudyInstanceUid.Equals(studyInstanceUid, StringComparison.OrdinalIgnoreCase)).FirstOrDefaultAsync().ConfigureAwait(false); + + return result; + }).ConfigureAwait(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _scope.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs b/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs new file mode 100644 index 000000000..dc9cc70f2 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/DicomDeidentifier.cs @@ -0,0 +1,101 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using Ardalis.GuardClauses; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + [PlugInName("Remote App Execution Outgoing")] + public class DicomDeidentifier : IOutputDataPlugIn + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly PlugInConfiguration _options; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public DicomDeidentifier( + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + IOptions configuration) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _options = configuration?.Value ?? throw new ArgumentNullException(nameof(configuration)); + + if (_options.RemoteAppConfigurations.ContainsKey(SR.ConfigKey_ReplaceTags) is false) + { + throw new ArgumentNullException(nameof(configuration)); + } + } + + public async Task<(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage)> ExecuteAsync(DicomFile dicomFile, ExportRequestDataMessage exportRequestDataMessage) + { + Guard.Against.Null(dicomFile, nameof(dicomFile)); + Guard.Against.Null(exportRequestDataMessage, nameof(exportRequestDataMessage)); + + var tags = Utilities.GetTagArrayFromStringArray(_options.RemoteAppConfigurations[SR.ConfigKey_ReplaceTags]); + var studyInstanceUid = dicomFile.Dataset.GetSingleValue(DicomTag.StudyInstanceUID); + var seriesInstanceUid = dicomFile.Dataset.GetSingleValue(DicomTag.SeriesInstanceUID); + + var scope = _serviceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + + var existing = await repository.GetAsync(exportRequestDataMessage.WorkflowInstanceId, exportRequestDataMessage.ExportTaskId, studyInstanceUid, seriesInstanceUid).ConfigureAwait(false); + + var newRecord = new RemoteAppExecution(exportRequestDataMessage, existing?.StudyInstanceUid, existing?.SeriesInstanceUid); + + newRecord.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), studyInstanceUid); + newRecord.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), seriesInstanceUid); + newRecord.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), dicomFile.Dataset.GetSingleValue(DicomTag.SOPInstanceUID)); + + dicomFile.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, newRecord.StudyInstanceUid); + dicomFile.Dataset.AddOrUpdate(DicomTag.SeriesInstanceUID, newRecord.SeriesInstanceUid); + dicomFile.Dataset.AddOrUpdate(DicomTag.SOPInstanceUID, newRecord.SopInstanceUid); + + foreach (var tag in tags) + { + if (tag.Equals(DicomTag.StudyInstanceUID) || + tag.Equals(DicomTag.SeriesInstanceUID) || + tag.Equals(DicomTag.SOPInstanceUID)) + { + continue; + } + + if (dicomFile.Dataset.TryGetString(tag, out var value)) + { + newRecord.OriginalValues.Add(tag.ToString(), value); + var newValue = Utilities.GetTagProxyValue(tag); + dicomFile.Dataset.AddOrUpdate(tag, newValue); + _logger.ValueChanged(tag.ToString(), value, newValue); + } + } + + await repository.AddAsync(newRecord).ConfigureAwait(false); + + return (dicomFile, exportRequestDataMessage); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/DicomReidentifier.cs b/src/Plug-ins/RemoteAppExecution/DicomReidentifier.cs new file mode 100644 index 000000000..88e32e9c0 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/DicomReidentifier.cs @@ -0,0 +1,69 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + [PlugInName("Remote App Execution Incoming")] + public class DicomReidentifier : IInputDataPlugIn + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public string Name => GetType().GetCustomAttribute()?.Name ?? GetType().Name; + + public DicomReidentifier( + ILogger logger, + IServiceScopeFactory serviceScopeFactory) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + } + + public async Task<(DicomFile dicomFile, FileStorageMetadata fileMetadata)> ExecuteAsync(DicomFile dicomFile, FileStorageMetadata fileMetadata) + { + var scope = _serviceScopeFactory.CreateScope(); + var repository = scope.ServiceProvider.GetRequiredService(); + + var sopInstanceUid = dicomFile.Dataset.GetSingleValue(DicomTag.SOPInstanceUID); + var remoteAppExecution = await repository.GetAsync(sopInstanceUid).ConfigureAwait(false); + + if (remoteAppExecution is null) + { + _logger.IncomingInstanceNotFound(sopInstanceUid); + return (dicomFile, fileMetadata); + } + + foreach (var key in remoteAppExecution.OriginalValues.Keys) + { + dicomFile.Dataset.AddOrUpdate(DicomTag.Parse(key), remoteAppExecution.OriginalValues[key]); + } + + fileMetadata.WorkflowInstanceId = remoteAppExecution.WorkflowInstanceId; + fileMetadata.TaskId = remoteAppExecution.ExportTaskId; + fileMetadata.ChangeCorrelationId(_logger, remoteAppExecution.CorrelationId); + + return (dicomFile, fileMetadata); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/InternalVisibleTo.cs b/src/Plug-ins/RemoteAppExecution/InternalVisibleTo.cs new file mode 100644 index 000000000..ee5c4f7ae --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/InternalVisibleTo.cs @@ -0,0 +1,19 @@ +/* + * Copyright 2021-2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test")] diff --git a/src/Plug-ins/RemoteAppExecution/Log.10000.DataPlugins.cs b/src/Plug-ins/RemoteAppExecution/Log.10000.DataPlugins.cs new file mode 100644 index 000000000..c67025933 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Log.10000.DataPlugins.cs @@ -0,0 +1,29 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + public static partial class Log + { + [LoggerMessage(EventId = 10000, Level = LogLevel.Debug, Message = "Changed {tag} from {originalValue} to {newValue}.")] + public static partial void ValueChanged(this ILogger logger, string tag, string originalValue, string newValue); + + [LoggerMessage(EventId = 10001, Level = LogLevel.Error, Message = "Cannot find entry for incoming instance {sopInstanceUid}.")] + public static partial void IncomingInstanceNotFound(this ILogger logger, string sopInstanceUid); + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.Designer.cs b/src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.Designer.cs new file mode 100644 index 000000000..89ad68f62 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.Designer.cs @@ -0,0 +1,72 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Migrations +{ + [DbContext(typeof(RemoteAppExecutionDbContext))] + [Migration("20230818161328_R4_0.4.0")] + partial class R4_040 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.RemoteAppExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExportTaskId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginalValues") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RequestTime") + .HasColumnType("TEXT"); + + b.Property("SeriesInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SopInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "WorkflowInstanceId", "ExportTaskId", "StudyInstanceUid", "SeriesInstanceUid" }, "idx_remoteapp_all"); + + b.HasIndex(new[] { "SopInstanceUid" }, "idx_remoteapp_instance"); + + b.HasIndex(new[] { "WorkflowInstanceId", "ExportTaskId", "StudyInstanceUid" }, "idx_remoteapp_study"); + + b.ToTable("RemoteAppExecutions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.cs b/src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.cs new file mode 100644 index 000000000..1ff6d3487 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Migrations/20230818161328_R4_0.4.0.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Migrations +{ + public partial class R4_040 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RemoteAppExecutions", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + RequestTime = table.Column(type: "TEXT", nullable: false), + WorkflowInstanceId = table.Column(type: "TEXT", nullable: false), + ExportTaskId = table.Column(type: "TEXT", nullable: false), + CorrelationId = table.Column(type: "TEXT", nullable: false), + StudyInstanceUid = table.Column(type: "TEXT", nullable: false), + SeriesInstanceUid = table.Column(type: "TEXT", nullable: false), + SopInstanceUid = table.Column(type: "TEXT", nullable: false), + OriginalValues = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RemoteAppExecutions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "idx_remoteapp_all", + table: "RemoteAppExecutions", + columns: new[] { "WorkflowInstanceId", "ExportTaskId", "StudyInstanceUid", "SeriesInstanceUid" }); + + migrationBuilder.CreateIndex( + name: "idx_remoteapp_instance", + table: "RemoteAppExecutions", + column: "SopInstanceUid"); + + migrationBuilder.CreateIndex( + name: "idx_remoteapp_study", + table: "RemoteAppExecutions", + columns: new[] { "WorkflowInstanceId", "ExportTaskId", "StudyInstanceUid" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RemoteAppExecutions"); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Migrations/RemoteAppExecutionDbContextModelSnapshot.cs b/src/Plug-ins/RemoteAppExecution/Migrations/RemoteAppExecutionDbContextModelSnapshot.cs new file mode 100644 index 000000000..cecd44f2c --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Migrations/RemoteAppExecutionDbContextModelSnapshot.cs @@ -0,0 +1,70 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; + +#nullable disable + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Migrations +{ + [DbContext(typeof(RemoteAppExecutionDbContext))] + partial class RemoteAppExecutionDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.21"); + + modelBuilder.Entity("Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.RemoteAppExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExportTaskId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OriginalValues") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RequestTime") + .HasColumnType("TEXT"); + + b.Property("SeriesInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SopInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StudyInstanceUid") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "WorkflowInstanceId", "ExportTaskId", "StudyInstanceUid", "SeriesInstanceUid" }, "idx_remoteapp_all"); + + b.HasIndex(new[] { "SopInstanceUid" }, "idx_remoteapp_instance"); + + b.HasIndex(new[] { "WorkflowInstanceId", "ExportTaskId", "StudyInstanceUid" }, "idx_remoteapp_study"); + + b.ToTable("RemoteAppExecutions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj b/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj new file mode 100644 index 000000000..c2c3896ab --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.csproj @@ -0,0 +1,64 @@ + + + + + + Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution + net6.0 + enable + enable + Apache-2.0 + true + True + latest + ..\..\.sonarlint\project-monai_monai-deploy-informatics-gatewaycsharp.ruleset + false + true + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs b/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs new file mode 100644 index 000000000..1af5ce355 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/RemoteAppExecution.cs @@ -0,0 +1,85 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.Json.Serialization; +using FellowOakDicom; +using Monai.Deploy.InformaticsGateway.Api; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + public class RemoteAppExecution + { + /// + /// Gets the ID of this record. + /// + [JsonPropertyName("_id")] + public Guid Id { get; init; } = Guid.NewGuid(); + + /// + /// Gets the date time this record is created. + /// + public DateTimeOffset RequestTime { get; init; } = DateTime.UtcNow; + + /// + /// Gets or sets the workflow instance ID of the original request. + /// + public string WorkflowInstanceId { get; set; } = string.Empty; + + /// + /// Gets or sets the export task ID of the original request. + /// + public string ExportTaskId { get; set; } = string.Empty; + + /// + /// Gets or sets the correlation ID of the original request. + /// + public string CorrelationId { get; set; } = string.Empty; + + ///// + ///// Gets or sets the proxy value of Study Instance UID. + ///// + public string StudyInstanceUid { get; set; } = string.Empty; + + ///// + ///// Gets or sets the proxy value of Series Instance UID. + ///// + public string SeriesInstanceUid { get; set; } = string.Empty; + + ///// + ///// Gets or sets the proxy value of SOP Instance UID. + ///// + public string SopInstanceUid { get; set; } = string.Empty; + + /// + /// Gets or sets the original values of a given DICOM tag. + /// + public Dictionary OriginalValues { get; init; } = new(); + + public RemoteAppExecution() + { } + + public RemoteAppExecution(ExportRequestDataMessage exportRequestDataMessage, string? studyInstanceUid, string? seriesInstanceUid) + { + WorkflowInstanceId = exportRequestDataMessage.WorkflowInstanceId; + ExportTaskId = exportRequestDataMessage.ExportTaskId; + CorrelationId = exportRequestDataMessage.CorrelationId; + + StudyInstanceUid = studyInstanceUid ?? Utilities.GetTagProxyValue(DicomTag.StudyInstanceUID); + SeriesInstanceUid = seriesInstanceUid ?? Utilities.GetTagProxyValue(DicomTag.SeriesInstanceUID); + SopInstanceUid = Utilities.GetTagProxyValue(DicomTag.SOPInstanceUID); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/SR.cs b/src/Plug-ins/RemoteAppExecution/SR.cs new file mode 100644 index 000000000..1ae61b7a5 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/SR.cs @@ -0,0 +1,23 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + internal static class SR + { + public static string ConfigKey_ReplaceTags = "ReplaceTags"; + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs new file mode 100644 index 000000000..9464cc8db --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/DatabaseRegistrarTest.cs @@ -0,0 +1,66 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.DependencyInjection; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Moq; +using Xunit; +using MongoDbTypes = Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database +{ + public class DatabaseRegistrarTest + { + [Fact] + public void GivenEntityFrameworkDatabaseType_WhenConfigureIsCalled_AddsDependencies() + { + var serviceDescriptors = new List(); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + serviceCollection.Setup(p => p.GetEnumerator()).Returns(serviceDescriptors.GetEnumerator()); + + var registrar = new DatabaseRegistrar(); + var returnedServiceCollection = registrar.Configure(serviceCollection.Object, DatabaseType.EntityFramework, "DataSource=file::memory:?cache=shared"); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(5)); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(RemoteAppExecutionDbContext))), Times.Once()); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IDatabaseMigrationManagerForPlugIns) && p.ImplementationType == typeof(MigrationManager))), Times.Once()); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IRemoteAppExecutionRepository) && p.ImplementationType == typeof(RemoteAppExecutionRepository))), Times.Once()); + } + + [Fact] + public void GivenMongoDatabaseType_WhenConfigureIsCalled_AddsDependencies() + { + var serviceDescriptors = new List(); + var serviceCollection = new Mock(); + serviceCollection.Setup(p => p.Add(It.IsAny())); + serviceCollection.Setup(p => p.GetEnumerator()).Returns(serviceDescriptors.GetEnumerator()); + + var registrar = new DatabaseRegistrar(); + var returnedServiceCollection = registrar.Configure(serviceCollection.Object, DatabaseType.MongoDb, "DataSource=file::memory:?cache=shared"); + + Assert.Same(serviceCollection.Object, returnedServiceCollection); + + serviceCollection.Verify(p => p.Add(It.IsAny()), Times.Exactly(2)); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IDatabaseMigrationManagerForPlugIns) && p.ImplementationType == typeof(MongoDbTypes.MigrationManager))), Times.Once()); + serviceCollection.Verify(p => p.Add(It.Is(p => p.ServiceType == typeof(IRemoteAppExecutionRepository) && p.ImplementationType == typeof(MongoDbTypes.RemoteAppExecutionRepository))), Times.Once()); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs new file mode 100644 index 000000000..6c7a95a71 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/MigrationManagerTest.cs @@ -0,0 +1,57 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.EntityFramework +{ + public class MigrationManagerTest + { + private readonly Mock _host; + private readonly RemoteAppExecutionDbContext _dbContext; + private readonly IServiceProvider _serviceProvider; + + public MigrationManagerTest() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=file:memdbmigration?mode=memory&cache=shared") + .Options; + + _host = new Mock(); + _dbContext = new RemoteAppExecutionDbContext(options); + + var services = new ServiceCollection(); + services.AddScoped(p => _dbContext); + + _serviceProvider = services.BuildServiceProvider(); + _host.Setup(p => p.Services).Returns(_serviceProvider); + } + + [Fact] + public void GivenARemoteAppExecutionDbContext_OnMigration_MigratesSuccessfully() + { + var mgr = new MigrationManager(); + var result = mgr.Migrate(_host.Object); + + Assert.Same(_host.Object, result); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs new file mode 100644 index 000000000..1ba57f920 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/RemoteAppExecutionRepositoryTest.cs @@ -0,0 +1,169 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using FellowOakDicom; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.EntityFramework +{ + [Collection("SqliteDatabase")] + public class RemoteAppExecutionRepositoryTest + { + private readonly SqliteDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public RemoteAppExecutionRepositoryTest(SqliteDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithRemoteAppExecutions(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.DatabaseContext); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenAddingToDatabase_ExpectItToBeSaved() + { + var record = new RemoteAppExecution + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + await store.AddAsync(record).ConfigureAwait(false); + var actual = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.Id.Equals(record.Id)).ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(record.CorrelationId, actual!.CorrelationId); + Assert.Equal(record.ExportTaskId, actual!.ExportTaskId); + Assert.Equal(record.Id, actual!.Id); + Assert.Equal(record.RequestTime, actual!.RequestTime); + Assert.Equal(record.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenRemoveIsCalled_ExpectItToDeleted() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var record = _databaseFixture.RemoteAppExecutions.First(); + var expected = await store.GetAsync(record.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(expected); + + var actual = await store.RemoveAsync(expected!).ConfigureAwait(false); + Assert.Same(expected, actual); + + var dbResult = await _databaseFixture.DatabaseContext.Set().FirstOrDefaultAsync(p => p.Id == record.Id).ConfigureAwait(false); + Assert.Null(dbResult); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithSopInstanceUid_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithStudyAndSeriesUids_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, expected.SeriesInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithRandomSeries_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, DicomUIDGenerator.GenerateDerivedFromUUID().UID).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs new file mode 100644 index 000000000..697916608 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/EntityFramework/SqliteDatabaseFixture.cs @@ -0,0 +1,87 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using FellowOakDicom; +using Microsoft.EntityFrameworkCore; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.EntityFramework; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.EntityFramework +{ + [CollectionDefinition("SqliteDatabase")] + public class SqliteDatabaseCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } + + public class SqliteDatabaseFixture + { + public RemoteAppExecutionDbContext DatabaseContext { get; set; } + public IList RemoteAppExecutions { get; init; } + + public SqliteDatabaseFixture() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=file::memory:?cache=shared") + .Options; + DatabaseContext = new RemoteAppExecutionDbContext(options); + DatabaseContext.Database.EnsureCreated(); + + RemoteAppExecutions = new List(); + } + + public void Dispose() + { + DatabaseContext.Dispose(); + } + + internal void InitDatabaseWithRemoteAppExecutions() + { + var set = DatabaseContext.Set(); + set.RemoveRange(set.ToList()); + RemoteAppExecutions.Clear(); + for (var i = 0; i < 5; i++) + { + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var record = new RemoteAppExecution + { + WorkflowInstanceId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), studyInstanceUid); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + set.Add(record); + RemoteAppExecutions.Add(record); + } + + DatabaseContext.SaveChanges(); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs new file mode 100644 index 000000000..119cf18bd --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/MongoDatabaseFixture.cs @@ -0,0 +1,90 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using FellowOakDicom; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Database.Api; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb; +using MongoDB.Driver; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.MongoDb +{ + [CollectionDefinition("MongoDatabase")] + public class MongoDatabaseCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } + + public class MongoDatabaseFixture + { + public IMongoClient Client { get; set; } + public IMongoDatabase Database { get; set; } + public IOptions Options { get; set; } + public IList RemoteAppExecutions { get; init; } + + public MongoDatabaseFixture() + { + Client = new MongoClient("mongodb://root:rootpassword@localhost:27017"); + Options = Microsoft.Extensions.Options.Options.Create(new DatabaseOptions { DatabaseName = $"IGTest" }); + Database = Client.GetDatabase(Options.Value.DatabaseName); + + var migration = new MigrationManager(); + migration.Migrate(null!); + + RemoteAppExecutions = new List(); + } + + internal void InitDatabaseWithRemoteAppExecutions() + { + var collection = Database.GetCollection(nameof(RemoteAppExecution)); + Clear(collection); + RemoteAppExecutions.Clear(); + for (var i = 0; i < 5; i++) + { + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var record = new RemoteAppExecution + { + WorkflowInstanceId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), studyInstanceUid); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + collection.InsertOne(record); + RemoteAppExecutions.Add(record); + } + } + + public static void Clear(IMongoCollection collection) where T : class + { + collection.DeleteMany(Builders.Filter.Empty); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs new file mode 100644 index 000000000..34ef85388 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Database/MongoDb/RemoteAppExecutionRepositoryTest.cs @@ -0,0 +1,173 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using FellowOakDicom; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database.MongoDb; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.Database.MongoDb +{ + [Collection("MongoDatabase")] + public class RemoteAppExecutionRepositoryTest + { + private readonly MongoDatabaseFixture _databaseFixture; + + private readonly Mock _serviceScopeFactory; + private readonly Mock> _logger; + private readonly IOptions _options; + + private readonly Mock _serviceScope; + private readonly IServiceProvider _serviceProvider; + + public RemoteAppExecutionRepositoryTest(MongoDatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); + _databaseFixture.InitDatabaseWithRemoteAppExecutions(); + + _serviceScopeFactory = new Mock(); + _logger = new Mock>(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + + _serviceScope = new Mock(); + var services = new ServiceCollection(); + services.AddScoped(p => _logger.Object); + services.AddScoped(p => databaseFixture.Client); + + _serviceProvider = services.BuildServiceProvider(); + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 }; + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenAddingToDatabase_ExpectItToBeSaved() + { + var record = new RemoteAppExecution + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + StudyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + SopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + await store.AddAsync(record).ConfigureAwait(false); + + var collection = _databaseFixture.Database.GetCollection(nameof(RemoteAppExecution)); + var actual = await collection.Find(p => p.Id == record.Id).FirstOrDefaultAsync().ConfigureAwait(false); + + Assert.NotNull(actual); + Assert.Equal(record.CorrelationId, actual!.CorrelationId); + Assert.Equal(record.ExportTaskId, actual!.ExportTaskId); + Assert.Equal(record.Id, actual!.Id); + Assert.Equal(record.RequestTime, actual!.RequestTime); + Assert.Equal(record.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenRemoveIsCalled_ExpectItToDeleted() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var record = _databaseFixture.RemoteAppExecutions.First(); + var expected = await store.GetAsync(record.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(expected); + + var actual = await store.RemoveAsync(expected!).ConfigureAwait(false); + Assert.Same(expected, actual); + + var collection = _databaseFixture.Database.GetCollection(nameof(RemoteAppExecution)); + var dbResult = await collection.Find(p => p.Id == record.Id).FirstOrDefaultAsync().ConfigureAwait(false); + Assert.Null(dbResult); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithSopInstanceUid_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.SopInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithStudyAndSeriesUids_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, expected.SeriesInstanceUid).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + + [Fact] + public async Task GivenARemoteAppExecution_WhenGetAsyncIsCalledWithRandomSeries_ExpectItToBeReturned() + { + var store = new RemoteAppExecutionRepository(_serviceScopeFactory.Object, _logger.Object, _options, _databaseFixture.Options); + + var expected = _databaseFixture.RemoteAppExecutions.First(); + var actual = await store.GetAsync(expected.WorkflowInstanceId, expected.ExportTaskId, expected.StudyInstanceUid, DicomUIDGenerator.GenerateDerivedFromUUID().UID).ConfigureAwait(false); + Assert.NotNull(actual); + Assert.Equal(expected.SopInstanceUid, actual.SopInstanceUid); + Assert.Equal(expected.StudyInstanceUid, actual.StudyInstanceUid); + Assert.Equal(expected.SeriesInstanceUid, actual.SeriesInstanceUid); + Assert.Equal(expected.WorkflowInstanceId, actual.WorkflowInstanceId); + Assert.Equal(expected.ExportTaskId, actual.ExportTaskId); + Assert.Equal(expected.RequestTime, actual.RequestTime); + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.CorrelationId, actual.CorrelationId); + Assert.Equal(expected.OriginalValues, actual.OriginalValues); + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs b/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs new file mode 100644 index 000000000..e5d2605a2 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/DicomDeidentifierTest.cs @@ -0,0 +1,262 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.InformaticsGateway.Api; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Configuration; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test +{ + public class DicomDeidentifierTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly ServiceCollection _serviceCollection; + private readonly Mock _repository; + private readonly IOptions _options; + private readonly Mock _serviceScope; + private readonly ServiceProvider _serviceProvider; + + public DicomDeidentifierTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _repository = new Mock(); + _serviceScope = new Mock(); + _options = Options.Create(new PlugInConfiguration()); + + _serviceCollection = new ServiceCollection(); + _serviceCollection.AddScoped(p => _logger.Object); + _serviceCollection.AddScoped(p => _repository.Object); + + _serviceProvider = _serviceCollection.BuildServiceProvider(); + + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void GivenDicomDeidentifier_TestConstructors() + { + Assert.Throws(() => new DicomDeidentifier(null, null, null)); + Assert.Throws(() => new DicomDeidentifier(_logger.Object, null, null)); + Assert.Throws(() => new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, null)); + Assert.Throws(() => new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, _options)); + + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, "tag1, tag2"); + var app = new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, _options); + + Assert.Equal(app.Name, app.GetType().GetCustomAttribute()!.Name); + } + + [Fact] + public async Task GivenEmptyReplaceTags_WhenExecuteIsCalledWithoutExistingRecords_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, string.Empty); + var app = new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecord(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GivenReplaceTags_WhenExecuteIsCalledWithoutExistingRecords_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, "StudyInstanceUID,AccessionNumber,PatientID,PatientName"); + + var app = new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var accessionNumber = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + var patientId = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + var patientName = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16); + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + dicom.Dataset.AddOrUpdate(DicomTag.AccessionNumber, accessionNumber); + dicom.Dataset.AddOrUpdate(DicomTag.PatientID, patientId); + dicom.Dataset.AddOrUpdate(DicomTag.PatientName, patientName); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecordWithAdditionalTags(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid, accessionNumber, patientId, patientName)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GivenExistingRecordWithSameStudy_WhenExecuteIsCalled_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, string.Empty); + var app = new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new RemoteAppExecution + { + WorkflowInstanceId = exportRequest.WorkflowInstanceId, + ExportTaskId = exportRequest.ExportTaskId, + StudyInstanceUid = studyInstanceUid, + SeriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID + }); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecord(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GivenExistingRecordWithSameSeries_WhenExecuteIsCalled_ExpectAsync() + { + _options.Value.RemoteAppConfigurations.Add(SR.ConfigKey_ReplaceTags, string.Empty); + var app = new DicomDeidentifier(_logger.Object, _serviceScopeFactory.Object, _options); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var exportRequest = GenerateExportRequest(); + var message = new ExportRequestDataMessage(exportRequest, "file.dcm"); + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new RemoteAppExecution + { + WorkflowInstanceId = exportRequest.WorkflowInstanceId, + ExportTaskId = exportRequest.ExportTaskId, + StudyInstanceUid = studyInstanceUid, + SeriesInstanceUid = seriesInstanceUid + }); + + _ = await app.ExecuteAsync(dicom, message).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync( + It.Is(p => p == exportRequest.WorkflowInstanceId), + It.Is(p => p == exportRequest.ExportTaskId), + It.Is(p => p == studyInstanceUid), + It.Is(p => p == seriesInstanceUid), + It.IsAny()), Times.Once()); + + _repository.Verify(p => p.AddAsync( + It.Is(p => AssertRecord(p, dicom, exportRequest, studyInstanceUid, seriesInstanceUid, sopInstanceUid)), + It.IsAny()), Times.Once()); + } + + private bool AssertRecord( + RemoteAppExecution record, + DicomFile dicom, + ExportRequestEvent exportRequest, + string studyInstanceUid, + string seriesInstanceUid, + string sopInstanceUid) + { + return record.WorkflowInstanceId == exportRequest.WorkflowInstanceId && + record.ExportTaskId == exportRequest.ExportTaskId && + record.OriginalValues[DicomTag.StudyInstanceUID.ToString()] == studyInstanceUid && + record.OriginalValues[DicomTag.SeriesInstanceUID.ToString()] == seriesInstanceUid && + record.OriginalValues[DicomTag.SOPInstanceUID.ToString()] == sopInstanceUid && + + record.StudyInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.StudyInstanceUID) && + record.SeriesInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SeriesInstanceUID) && + record.SopInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SOPInstanceUID); + } + + private bool AssertRecordWithAdditionalTags( + RemoteAppExecution record, + DicomFile dicom, + ExportRequestEvent exportRequest, + string studyInstanceUid, + string seriesInstanceUid, + string sopInstanceUid, + string accessionNumber, + string patientId, + string patientName) + { + return record.WorkflowInstanceId == exportRequest.WorkflowInstanceId && + record.ExportTaskId == exportRequest.ExportTaskId && + record.OriginalValues[DicomTag.StudyInstanceUID.ToString()] == studyInstanceUid && + record.OriginalValues[DicomTag.SeriesInstanceUID.ToString()] == seriesInstanceUid && + record.OriginalValues[DicomTag.SOPInstanceUID.ToString()] == sopInstanceUid && + record.OriginalValues[DicomTag.AccessionNumber.ToString()] == accessionNumber && + record.OriginalValues[DicomTag.PatientID.ToString()] == patientId && + record.OriginalValues[DicomTag.PatientName.ToString()] == patientName && + + record.StudyInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.StudyInstanceUID) && + record.SeriesInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SeriesInstanceUID) && + record.SopInstanceUid == dicom.Dataset.GetSingleValue(DicomTag.SOPInstanceUID); + } + + private ExportRequestEvent GenerateExportRequest() => + new() + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + WorkflowInstanceId = Guid.NewGuid().ToString(), + }; + } +} diff --git a/src/Plug-ins/RemoteAppExecution/Test/DicomReidentifierTest.cs b/src/Plug-ins/RemoteAppExecution/Test/DicomReidentifierTest.cs new file mode 100644 index 000000000..9948db359 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/DicomReidentifierTest.cs @@ -0,0 +1,134 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Reflection; +using FellowOakDicom; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Api.PlugIns; +using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Database; +using Monai.Deploy.InformaticsGateway.SharedTest; +using Monai.Deploy.Messaging.Events; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test +{ + public class DicomReidentifierTest + { + private readonly Mock> _logger; + private readonly Mock _serviceScopeFactory; + private readonly ServiceCollection _serviceCollection; + private readonly Mock _repository; + private readonly Mock _serviceScope; + private readonly ServiceProvider _serviceProvider; + + public DicomReidentifierTest() + { + _logger = new Mock>(); + _serviceScopeFactory = new Mock(); + _repository = new Mock(); + _serviceScope = new Mock(); + + _serviceCollection = new ServiceCollection(); + _serviceCollection.AddScoped(p => _logger.Object); + _serviceCollection.AddScoped(p => _repository.Object); + + _serviceProvider = _serviceCollection.BuildServiceProvider(); + + _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); + _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + + _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + } + + [Fact] + public void GivenDicomDeidentifier_TestConstructors() + { + Assert.Throws(() => new DicomReidentifier(null, null)); + Assert.Throws(() => new DicomReidentifier(_logger.Object, null)); + + var app = new DicomReidentifier(_logger.Object, _serviceScopeFactory.Object); + + Assert.Equal(app.Name, app.GetType().GetCustomAttribute()!.Name); + } + + [Fact] + public async Task GivenIncomingInstance_WhenExecuteIsCalledWithMissingRecord_ExpectErrorToBeLogged() + { + var app = new DicomReidentifier(_logger.Object, _serviceScopeFactory.Object); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + var metadata = new DicomFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), studyInstanceUid, seriesInstanceUid, sopInstanceUid, DataService.DIMSE, "calling", "called"); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny())).ReturnsAsync(default(RemoteAppExecution)); + + _ = await app.ExecuteAsync(dicom, metadata).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync(It.IsAny(), It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Cannot find entry for incoming instance {sopInstanceUid}.", LogLevel.Error, Times.Once()); + } + + [Fact] + public async Task GivenIncomingInstance_WhenExecuteIsCalledWithRecord_ExpectDataToBeFilled() + { + var app = new DicomReidentifier(_logger.Object, _serviceScopeFactory.Object); + + var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var seriesInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var sopInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + var dicom = InstanceGenerator.GenerateDicomFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + var metadata = new DicomFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), studyInstanceUid, seriesInstanceUid, sopInstanceUid, DataService.DIMSE, "calling", "called"); + var record = new RemoteAppExecution + { + CorrelationId = Guid.NewGuid().ToString(), + ExportTaskId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid(), + RequestTime = DateTimeOffset.UtcNow, + }; + + record.OriginalValues.Add(DicomTag.StudyInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SeriesInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.SOPInstanceUID.ToString(), DicomUIDGenerator.GenerateDerivedFromUUID().UID); + record.OriginalValues.Add(DicomTag.PatientID.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.AccessionNumber.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + record.OriginalValues.Add(DicomTag.StudyDescription.ToString(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16)); + + _repository.Setup(p => p.GetAsync(It.IsAny(), It.IsAny())).ReturnsAsync(record); + + _ = await app.ExecuteAsync(dicom, metadata).ConfigureAwait(false); + + _repository.Verify(p => p.GetAsync(It.IsAny(), It.IsAny()), Times.Once()); + + _logger.VerifyLogging($"Cannot find entry for incoming instance {sopInstanceUid}.", LogLevel.Error, Times.Never()); + + Assert.Equal(record.OriginalValues[DicomTag.StudyInstanceUID.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.SeriesInstanceUID.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.PatientID.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.AccessionNumber.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty)); + Assert.Equal(record.OriginalValues[DicomTag.StudyDescription.ToString()], dicom.Dataset.GetSingleValueOrDefault(DicomTag.StudyDescription, string.Empty)); + + Assert.Equal(record.CorrelationId, metadata.CorrelationId); + Assert.Equal(record.ExportTaskId, metadata.TaskId); + Assert.Equal(record.WorkflowInstanceId, metadata.WorkflowInstanceId); + } + } +} \ No newline at end of file diff --git a/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj b/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj new file mode 100644 index 000000000..2d36c43f9 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test.csproj @@ -0,0 +1,64 @@ + + + + + + net6.0 + enable + enable + Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution.Test + false + Apache-2.0 + true + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json b/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json new file mode 100644 index 000000000..e8f565352 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Test/packages.lock.json @@ -0,0 +1,1601 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "tW3lsNS+dAEII6YGUX/VMoJjBS1QvsxqJeqLaJXub08y1FSjasFPtQ4UBUsudE9PNrzLjooClMsPtY2cZLdXpQ==" + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "NJq3pURTBBHWkHgYkZJlCesZ6udyQIlnS2gU8SdR0xZ5VhW3c90tCCkZel38CgPmq29vWfxLurJLEwroIUokzg==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.21" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "iAs1F5gxEQRRGNHDKJ6ZtoSbOAWcjdk+mABEIy2vRLeACp7xBPdQRQdJnENmxykkBgOVef73RpU3xVdDcn8Omg==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.21", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "2If1Lt04gD+KrKPFbMUeUzB8Av/EGJJFxNLGfC/CKLgy8+jAYsamyQ/Hux+93XCajJxFLnJimqSg+bBBvXX+2g==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "6.0.21", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", + "Microsoft.Extensions.DependencyModel": "6.0.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.7.1, )", + "resolved": "17.7.1", + "contentHash": "o1qyqDOR8eMuQrC1e5EMMcE+Wm3rwES5aHNWaJpi2A5qwVOru23zsdXkndT6hgl79QsJsqKp+/RNcayIzpHjvA==", + "dependencies": { + "Microsoft.CodeCoverage": "17.7.1", + "Microsoft.TestPlatform.TestHost": "17.7.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.69, )", + "resolved": "4.20.69", + "contentHash": "8P/oAUOL8ZVyXnzBBcgdhTsOD1kQbAWfOcMI7KDQO3HqQtzB/0WYLdnMa4Jefv8nu/MQYiiG0IuoJdvG0v0Nig==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "System.IO.Abstractions.TestingHelpers": { + "type": "Direct", + "requested": "[17.2.3, )", + "resolved": "17.2.3", + "contentHash": "tkXvQbsfOIfeoGso+WptCuouFLiWt3EU8s0D8poqIVz1BJOOszkPuFbFgP2HUTJ9bp5n1HH89eFHILo6Oz5XUw==", + "dependencies": { + "System.IO.Abstractions": "17.2.3" + } + }, + "xRetry": { + "type": "Direct", + "requested": "[1.9.0, )", + "resolved": "1.9.0", + "contentHash": "NeIbJrwpc5EUPagx/mdd/7KzpR36BO8IWrsbgtvOVjxD2xtmNfUHieZ24PeZ4oCYiLBcTviCy+og/bE/OvPchw==", + "dependencies": { + "xunit.core": "[2.4.0, 3.0.0)" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "f2V5wuAdoaq0mRTt9UBmPbVex9HcwFYn+y7WaKUz5Xpakcrv7lhtQWBJUWNY4N3Z+o+atDBLyAALM1QWx04C6Q==", + "dependencies": { + "xunit.analyzers": "1.2.0", + "xunit.assert": "2.5.0", + "xunit.core": "[2.5.0]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "+Gp9vuC2431yPyKB15YrOTxCuEAErBQUTIs6CquumX1F073UaPHGW0VE/XVJLMh9W4sXdz3TBkcHdFWZrRn2Hw==" + }, + "Ardalis.GuardClauses": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "+UcJ2s+gf2wMNrwadCaHZV2DMcGgBU1t22A+jm40P4MHQRLy9hcleGy5xdVWd4dXZPa5Vlp4TG5xU2rhoDYrBA==" + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "3.7.200.13", + "contentHash": "yiUuhTI8w183euRqhXym1DyhnD/1ccxceRoruCfkIoqY3PAaFgFL8pE4iDLDZa7SUW4M4qZnQ5PMlFr1rrl6zw==" + }, + "AWSSDK.SecurityToken": { + "type": "Transitive", + "resolved": "3.7.201.9", + "contentHash": "yKlTPrvNeDdzkOX82Ydf7MD09Gk3dK74JWZPRWJ3QIxskWVoNTAyLvfVBzbi+/fGnjf8/qKsSzxT7GHLqds37A==", + "dependencies": { + "AWSSDK.Core": "[3.7.200.13, 4.0.0)" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "CommunityToolkit.HighPerformance": { + "type": "Transitive", + "resolved": "8.2.0", + "contentHash": "iKzsPiSnXoQUN5AoApYmdfnLn9osNb+YCLWRr5PFmrDEQVIu7OeOyf4DPvNBvbqbYLZCfvHozPkulyv6zBQsFw==" + }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "fo-dicom": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "YraR81u9XuTN7l+pt6HzT0KvuhgWVZ9LCuHMH3zgFfAtv4peT1y+nYMSGwF9YqNP+oZnzh0s0PJ+vJMsTDpGIw==", + "dependencies": { + "CommunityToolkit.HighPerformance": "8.2.0", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Buffers": "4.5.1", + "System.Text.Encoding.CodePages": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.7", + "System.Threading.Channels": "6.0.0" + } + }, + "Macross.Json.Extensions": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "AkNshs6dopj8FXsmkkJxvLivN2SyDJQDbjcds5lo9+Y6L4zpcoXdmzXQ3VVN+AIWQr0CTD5A7vkuHGAr2aypZg==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.7.1", + "contentHash": "NmGwM2ZJy4CAMdJYIp53opUjnXsMbzASX5oQzgxORicJsgz5Lp50fnRI8OmQ/kYNg6dHfr3IjuUoXbsotDX+KA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "9S+kvYcPyGBqH5KX7sL0d7xYADTUrUVaBz+GZsSx4N4jKh/0mka6IFdeuFYzs3T6wdtHTvzdltcRwucwuTFpdw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "XUPcDrn/Vrv9yF4M3b9FYEZvqW1gyS3hfJhFiP0pttuRYnGRB+y3/6g/9k0GIoU62+XkxGa78l1JUccq1uvAXQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.21", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.21", + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Collections.Immutable": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.1" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "GlNsy7qoFnCxgZlPpb8H/Srq1juOiV6W5XaijSA0+h8V0yn1VJ0owjb01If3di3Covs/8682A+ByTFjmEUxePA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "gS8tH419vOY2kEyqEZBX8VnXWmtHaor7gVx6zVaXCsEyQurGR/aVB++IZ62vzeQFS9R46LbNY6D6bqEA6j3iCg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "Ev5FM2KpXJu7+Dm9qvLf1FhSJMytrhSXho92Vompmgeiz3p4InldluidmKKmv8nZQAjs9dTCUUyvk1pxQjysaQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.21", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bcz5sSFJbganH0+YrfvIjJDIcKNW7TL07C4d1eTmXy/wOt52iz4LVogJb6pazs7W0+74j0YpXFErvp++Aq5Bsw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "B4y+Cev05eMcjf1na0v9gza6GUtahXbtY1JCypIgx3B4Ea/KAgsWyXEmW4q6zMbmTMtKzmPVk09rvFJirvMwTg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "BUyFU9t+HzlSE7ri4B+AQN2BgTgHv/uM82s5ZkgU1BApyzWzIl48nDsG5wR1t0pniNuuyTBzG3qCW8152/NtSw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "V4Dth2cYMZpw3HhGw9XUDIijpI6gN+22LDt0AhufIgOppCUfpWX4483OmN+dFXRJkJLc8Tv0Q8QK+1ingT2+KQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Physical": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GJGery6QytCzS/BxJ96klgG9in3uH26KcUBbiVG/coNDXCRq6LGVVlUT4vXq34KPuM+R2av+LeYdX9h4IZOCUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "vWXPg3HJQIpZkENn1KWq8SfbqVujVD7S7vIAyFXXqK5xkf1Vho+vG0bLBCHxU36lD1cLLtmGpfYf0B3MYFi9tQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "1Qf/tEg6IlzbvCxrc+pZE+ZGrajBdB/+V2+abeAu6lg8wXGHmO8JtnrNqwc5svSbcz3udxinUPyH3vw6ZujKbg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21", + "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "5FSA1euCRtbRqVgTn2ahgCG9Cy29UQXAZMCJLUlrQQaC5rko0+d/aq9SiFGIDP7cvoWUsatrlNdfc6UyOMV5aA==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "QvkL7l0nM8udt3gfyu0Vw8bbCXblxaKOl7c2oBfgGy4LCURRaL9XWZX1FWJrQc43oMokVneVxH38iz+bY1sbhg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.7.1", + "contentHash": "nDmV03yHIdAiG5J3ZEjMyJM2XDjmWORuKgbrGzqlAipBEjUuy5Z5S7WwSqUv9OiaUrtCn9dNYmjfMELUi08leQ==", + "dependencies": { + "NuGet.Frameworks": "6.5.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.7.1", + "contentHash": "WCU1NyBarz0tih+I9K5OWN1dVo3z562Iek/VAqWNWRFWw1GeUGqB61iixrBvZO77sjTtBc1cXO8H95uImfmEdw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.7.1", + "Newtonsoft.Json": "13.0.1" + } + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Monai.Deploy.Messaging": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "Xr1V3ZrSJByfUP4w+aiOAqC7Uzt1GqRXj35qSTQs9C1oI4gCiBN4wnre0SSvoA7vHQNZPGWNWXtiqbI7Cov3Mg==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Newtonsoft.Json": "13.0.3", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+1JX7QDgVEMqYA0/M1QMr1gtXRC6lEuhBtLfJXWi6cEgh9kOPE0KiHd1AWI7PxBgBbsEBZaNQSvWqShlwcu6bA==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.201.9", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Monai.Deploy.Storage.S3Policy": "0.2.18", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage.S3Policy": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+b0nDnf4OoajdH2hB02elRC6G+GjlYnxJC+F3dGbUUXGMtPApzs8c8s/EG4fKzihxsVovJtqnJl7atcaPyl12Q==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MongoDB.Bson": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "QT+D1I3Jz6r6S6kCgJD1L9dRCLVJCKlkGRkA+tJ7uLpHRmjDNcNKy4D1T+L9gQrjl95lDN9PHdwEytdvCW/jzA==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, + "MongoDB.Driver": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "VxKj1wuhadiXhaXkykCWRgsYOysdaOYJ202hJFz25UjkrqC/tHA8RS4hdS5HYfGWoI//fypBXnxZCkEjXLXdfw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Driver.Core": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0" + } + }, + "MongoDB.Driver.Core": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "Ac44U3bQfinmdH5KNFjTidJe9LKW87SxkXJ3YuIUJQMITEc4083YF1yvjJxaSeYF9er0YgHSmwhHpsZv0Fwplg==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.100.14", + "DnsClient": "1.6.1", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0", + "SharpCompress": "0.30.1", + "Snappier": "1.0.0", + "System.Buffers": "4.5.1", + "ZstdSharp.Port": "0.6.2" + } + }, + "MongoDB.Libmongocrypt": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "fgNw8Dxpkq7mpoaAYes8cfnPRzvFIoB8oL9GPXwi3op/rONftl0WAeg4akRLcxfoVuUvuUO2wGoVBr3JzJ7Svw==" + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" + }, + "Polly": { + "type": "Transitive", + "resolved": "7.2.4", + "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SharpCompress": { + "type": "Transitive", + "resolved": "0.30.1", + "contentHash": "XqD4TpfyYGa7QTPzaGlMVbcecKnXy4YmYLDWrU+JIj7IuRNl7DH2END+Ll7ekWIY8o3dAMWLFDE1xdhfIWD1nw==" + }, + "Snappier": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "ilkvNhrTersLmIVAcDwwPqfhUFCg19Z1GVMvCSi3xk6Akq94f4qadLORQCq/T8+9JgMiPs+F/NECw5uauviaNw==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.2", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "A8EBepVqY2lnAp3a8jnhbgzF2tlj2S3HcJQGANTYg/TbYbKa8Z5cM1h74An/vy0svhfzT7tVY0sFmUglLgv+2g==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "zibGtku8M4Eea1R3ZCAxc86QbNvyEN17mAcQkvWKBuHvRpMiK2g5anG4R5Be7cWKSd1i6baYz8y4dMMAKcXKPg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "lxCZarZdvAsMl2zw9bXHrXK6RxVhB4b23iTFhCOdHFhxfbsxLxWf+ocvswJwR/9Wh/E//ddMi+wJGqUKV7VwoA==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "17.2.3", + "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "6.0.7", + "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "d3dehV/DASLRlR8stWQmbPPjfYC2tct50Evav+OlsJMkfFqkhYvzO1k0s81lk0px8O0knZU/FqC8SqbXOtn+hw==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "wN84pKX5jzfpgJ0bB6arrCA/oelBeYLCpnQ9Wj5xGEVPydKzVSDY5tEatFLHE/rO0+0RC+I4H5igGE118jRh1w==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "dnV0Mn2s1C0y2m33AylQyMkEyhBQsL4R0302kwSGiEGuY3JwzEmhTa9pnghyMRPliYSs4fXfkEAP+5bKXryGFg==", + "dependencies": { + "xunit.extensibility.core": "[2.5.0]", + "xunit.extensibility.execution": "[2.5.0]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "xRm6NIV3i7I+LkjsAJ91Xz2fxJm/oMEi2CYq1G5HlGTgcK1Zo2wNbLO6nKX1VG5FZzXibSdoLwr/MofVvh3mFA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.5.0", + "contentHash": "7+v2Bvp+1ew1iMGQVb1glICi8jcNdHbRUX6Ru0dmJBViGdjiS7kyqcX2VxleQhFbKNi+WF0an7/TeTXD283RlQ==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.extensibility.core": "[2.5.0]" + } + }, + "ZstdSharp.Port": { + "type": "Transitive", + "resolved": "0.6.2", + "contentHash": "jPao/LdUNLUz8rn3H1D8W7wQbZsRZM0iayvWI4xGejJg3XJHT56gcmYdgmCGPdJF1UEBqUjucCRrFB+4HbJsbw==" + }, + "monai.deploy.informaticsgateway.api": { + "type": "Project", + "dependencies": { + "Macross.Json.Extensions": "[3.0.0, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.21, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", + "Monai.Deploy.Messaging": "[1.0.0, )", + "Monai.Deploy.Storage": "[0.2.18, )", + "fo-dicom": "[5.1.1, )" + } + }, + "monai.deploy.informaticsgateway.common": { + "type": "Project", + "dependencies": { + "Ardalis.GuardClauses": "[4.1.1, )", + "System.IO.Abstractions": "[17.2.3, )" + } + }, + "monai.deploy.informaticsgateway.configuration": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )" + } + }, + "monai.deploy.informaticsgateway.database.api": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )" + } + }, + "monai.deploy.informaticsgateway.plugins.remoteappexecution": { + "type": "Project", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Relational": "[6.0.21, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[6.0.21, )", + "Microsoft.Extensions.Configuration": "[6.0.1, )", + "Microsoft.Extensions.Configuration.FileExtensions": "[6.0.0, )", + "Microsoft.Extensions.Configuration.Json": "[6.0.0, )", + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Database.Api": "[1.0.0, )", + "MongoDB.Driver": "[2.21.0, )", + "Polly": "[7.2.4, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Plug-ins/RemoteAppExecution/Utilities.cs b/src/Plug-ins/RemoteAppExecution/Utilities.cs new file mode 100644 index 000000000..8beb9cc94 --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/Utilities.cs @@ -0,0 +1,59 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Text.RegularExpressions; +using FellowOakDicom; + +namespace Monai.Deploy.InformaticsGateway.PlugIns.RemoteAppExecution +{ + internal static class Utilities + { + private static DicomTag GetDicomTagByName(string tag) => DicomDictionary.Default[tag] ?? DicomDictionary.Default[Regex.Replace(tag, @"\s+", "", RegexOptions.None, TimeSpan.FromSeconds(1))]; + + public static DicomTag[] GetTagArrayFromStringArray(string values) + { + var names = values.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return names.Select(n => GetDicomTagByName(n)).ToArray(); + } + + public static T? GetTagProxyValue(DicomTag tag) where T : class + { + // partial implementation for now see + // https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html + // for full list + switch (tag.DictionaryEntry.ValueRepresentations[0].Code) + { + case "UI": + case "LO": + case "LT": + { + return DicomUIDGenerator.GenerateDerivedFromUUID().UID as T; + } + case "SH": + case "AE": + case "CS": + case "PN": + case "ST": + case "UT": + { + return "no Value" as T; + } + default: + return default; + } + } + } +} diff --git a/src/Plug-ins/RemoteAppExecution/appsettings.json b/src/Plug-ins/RemoteAppExecution/appsettings.json new file mode 100644 index 000000000..57c93015b --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "InformaticsGatewayDatabase": "Data Source=mig.db" + } +} \ No newline at end of file diff --git a/src/Plug-ins/RemoteAppExecution/packages.lock.json b/src/Plug-ins/RemoteAppExecution/packages.lock.json new file mode 100644 index 000000000..a456207ba --- /dev/null +++ b/src/Plug-ins/RemoteAppExecution/packages.lock.json @@ -0,0 +1,577 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "XUPcDrn/Vrv9yF4M3b9FYEZvqW1gyS3hfJhFiP0pttuRYnGRB+y3/6g/9k0GIoU62+XkxGa78l1JUccq1uvAXQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "6.0.21", + "Microsoft.EntityFrameworkCore.Analyzers": "6.0.21", + "Microsoft.Extensions.Caching.Memory": "6.0.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Collections.Immutable": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.1" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "G+e0jPI1nD2DHszHXGqO57ogAVIKRy4930jCb7W/v2JfYKVcEbupzdYxEOQAGZws98MXllHNSqeb6fE1EW131A==", + "dependencies": { + "Humanizer.Core": "2.8.26", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "Ev5FM2KpXJu7+Dm9qvLf1FhSJMytrhSXho92Vompmgeiz3p4InldluidmKKmv8nZQAjs9dTCUUyvk1pxQjysaQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "6.0.21", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[6.0.21, )", + "resolved": "6.0.21", + "contentHash": "iAs1F5gxEQRRGNHDKJ6ZtoSbOAWcjdk+mABEIy2vRLeACp7xBPdQRQdJnENmxykkBgOVef73RpU3xVdDcn8Omg==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.21", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.2" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Direct", + "requested": "[6.0.1, )", + "resolved": "6.0.1", + "contentHash": "BUyFU9t+HzlSE7ri4B+AQN2BgTgHv/uM82s5ZkgU1BApyzWzIl48nDsG5wR1t0pniNuuyTBzG3qCW8152/NtSw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "V4Dth2cYMZpw3HhGw9XUDIijpI6gN+22LDt0AhufIgOppCUfpWX4483OmN+dFXRJkJLc8Tv0Q8QK+1ingT2+KQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Physical": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "GJGery6QytCzS/BxJ96klgG9in3uH26KcUBbiVG/coNDXCRq6LGVVlUT4vXq34KPuM+R2av+LeYdX9h4IZOCUg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "6.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "MongoDB.Driver": { + "type": "Direct", + "requested": "[2.21.0, )", + "resolved": "2.21.0", + "contentHash": "VxKj1wuhadiXhaXkykCWRgsYOysdaOYJ202hJFz25UjkrqC/tHA8RS4hdS5HYfGWoI//fypBXnxZCkEjXLXdfw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Driver.Core": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0" + } + }, + "Polly": { + "type": "Direct", + "requested": "[7.2.4, )", + "resolved": "7.2.4", + "contentHash": "bw00Ck5sh6ekduDE3mnCo1ohzuad946uslCDEENu3091+6UKnBuKLo4e+yaNcCzXxOZCXWY2gV4a35+K1d4LDA==" + }, + "Ardalis.GuardClauses": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "+UcJ2s+gf2wMNrwadCaHZV2DMcGgBU1t22A+jm40P4MHQRLy9hcleGy5xdVWd4dXZPa5Vlp4TG5xU2rhoDYrBA==" + }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "3.7.200.13", + "contentHash": "yiUuhTI8w183euRqhXym1DyhnD/1ccxceRoruCfkIoqY3PAaFgFL8pE4iDLDZa7SUW4M4qZnQ5PMlFr1rrl6zw==" + }, + "AWSSDK.SecurityToken": { + "type": "Transitive", + "resolved": "3.7.201.9", + "contentHash": "yKlTPrvNeDdzkOX82Ydf7MD09Gk3dK74JWZPRWJ3QIxskWVoNTAyLvfVBzbi+/fGnjf8/qKsSzxT7GHLqds37A==", + "dependencies": { + "AWSSDK.Core": "[3.7.200.13, 4.0.0)" + } + }, + "CommunityToolkit.HighPerformance": { + "type": "Transitive", + "resolved": "8.2.0", + "contentHash": "iKzsPiSnXoQUN5AoApYmdfnLn9osNb+YCLWRr5PFmrDEQVIu7OeOyf4DPvNBvbqbYLZCfvHozPkulyv6zBQsFw==" + }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "fo-dicom": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "YraR81u9XuTN7l+pt6HzT0KvuhgWVZ9LCuHMH3zgFfAtv4peT1y+nYMSGwF9YqNP+oZnzh0s0PJ+vJMsTDpGIw==", + "dependencies": { + "CommunityToolkit.HighPerformance": "8.2.0", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Bcl.HashCode": "1.1.1", + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Buffers": "4.5.1", + "System.Text.Encoding.CodePages": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.7", + "System.Threading.Channels": "6.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.8.26", + "contentHash": "OiKusGL20vby4uDEswj2IgkdchC1yQ6rwbIkZDVBPIR6al2b7n3pC91elBul9q33KaBgRKhbZH3+2Ur4fnWx2A==" + }, + "Macross.Json.Extensions": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "AkNshs6dopj8FXsmkkJxvLivN2SyDJQDbjcds5lo9+Y6L4zpcoXdmzXQ3VVN+AIWQr0CTD5A7vkuHGAr2aypZg==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "9S+kvYcPyGBqH5KX7sL0d7xYADTUrUVaBz+GZsSx4N4jKh/0mka6IFdeuFYzs3T6wdtHTvzdltcRwucwuTFpdw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "GlNsy7qoFnCxgZlPpb8H/Srq1juOiV6W5XaijSA0+h8V0yn1VJ0owjb01If3di3Covs/8682A+ByTFjmEUxePA==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "gS8tH419vOY2kEyqEZBX8VnXWmtHaor7gVx6zVaXCsEyQurGR/aVB++IZ62vzeQFS9R46LbNY6D6bqEA6j3iCg==" + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "2If1Lt04gD+KrKPFbMUeUzB8Av/EGJJFxNLGfC/CKLgy8+jAYsamyQ/Hux+93XCajJxFLnJimqSg+bBBvXX+2g==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "6.0.21", + "Microsoft.EntityFrameworkCore.Relational": "6.0.21", + "Microsoft.Extensions.DependencyModel": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bcz5sSFJbganH0+YrfvIjJDIcKNW7TL07C4d1eTmXy/wOt52iz4LVogJb6pazs7W0+74j0YpXFErvp++Aq5Bsw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "B4y+Cev05eMcjf1na0v9gza6GUtahXbtY1JCypIgx3B4Ea/KAgsWyXEmW4q6zMbmTMtKzmPVk09rvFJirvMwTg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "vWXPg3HJQIpZkENn1KWq8SfbqVujVD7S7vIAyFXXqK5xkf1Vho+vG0bLBCHxU36lD1cLLtmGpfYf0B3MYFi9tQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0", + "System.Text.Json": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "1Qf/tEg6IlzbvCxrc+pZE+ZGrajBdB/+V2+abeAu6lg8wXGHmO8JtnrNqwc5svSbcz3udxinUPyH3vw6ZujKbg==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.21", + "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "6.0.21", + "contentHash": "5FSA1euCRtbRqVgTn2ahgCG9Cy29UQXAZMCJLUlrQQaC5rko0+d/aq9SiFGIDP7cvoWUsatrlNdfc6UyOMV5aA==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "QvkL7l0nM8udt3gfyu0Vw8bbCXblxaKOl7c2oBfgGy4LCURRaL9XWZX1FWJrQc43oMokVneVxH38iz+bY1sbhg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Monai.Deploy.Messaging": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "Xr1V3ZrSJByfUP4w+aiOAqC7Uzt1GqRXj35qSTQs9C1oI4gCiBN4wnre0SSvoA7vHQNZPGWNWXtiqbI7Cov3Mg==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Newtonsoft.Json": "13.0.3", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+1JX7QDgVEMqYA0/M1QMr1gtXRC6lEuhBtLfJXWi6cEgh9kOPE0KiHd1AWI7PxBgBbsEBZaNQSvWqShlwcu6bA==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.201.9", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.21", + "Monai.Deploy.Storage.S3Policy": "0.2.18", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage.S3Policy": { + "type": "Transitive", + "resolved": "0.2.18", + "contentHash": "+b0nDnf4OoajdH2hB02elRC6G+GjlYnxJC+F3dGbUUXGMtPApzs8c8s/EG4fKzihxsVovJtqnJl7atcaPyl12Q==", + "dependencies": { + "Ardalis.GuardClauses": "4.1.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MongoDB.Bson": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "QT+D1I3Jz6r6S6kCgJD1L9dRCLVJCKlkGRkA+tJ7uLpHRmjDNcNKy4D1T+L9gQrjl95lDN9PHdwEytdvCW/jzA==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, + "MongoDB.Driver.Core": { + "type": "Transitive", + "resolved": "2.21.0", + "contentHash": "Ac44U3bQfinmdH5KNFjTidJe9LKW87SxkXJ3YuIUJQMITEc4083YF1yvjJxaSeYF9er0YgHSmwhHpsZv0Fwplg==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.100.14", + "DnsClient": "1.6.1", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "MongoDB.Bson": "2.21.0", + "MongoDB.Libmongocrypt": "1.8.0", + "SharpCompress": "0.30.1", + "Snappier": "1.0.0", + "System.Buffers": "4.5.1", + "ZstdSharp.Port": "0.6.2" + } + }, + "MongoDB.Libmongocrypt": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "fgNw8Dxpkq7mpoaAYes8cfnPRzvFIoB8oL9GPXwi3op/rONftl0WAeg4akRLcxfoVuUvuUO2wGoVBr3JzJ7Svw==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "SharpCompress": { + "type": "Transitive", + "resolved": "0.30.1", + "contentHash": "XqD4TpfyYGa7QTPzaGlMVbcecKnXy4YmYLDWrU+JIj7IuRNl7DH2END+Ll7ekWIY8o3dAMWLFDE1xdhfIWD1nw==" + }, + "Snappier": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "ilkvNhrTersLmIVAcDwwPqfhUFCg19Z1GVMvCSi3xk6Akq94f4qadLORQCq/T8+9JgMiPs+F/NECw5uauviaNw==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.2", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "A8EBepVqY2lnAp3a8jnhbgzF2tlj2S3HcJQGANTYg/TbYbKa8Z5cM1h74An/vy0svhfzT7tVY0sFmUglLgv+2g==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "zibGtku8M4Eea1R3ZCAxc86QbNvyEN17mAcQkvWKBuHvRpMiK2g5anG4R5Be7cWKSd1i6baYz8y4dMMAKcXKPg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "lxCZarZdvAsMl2zw9bXHrXK6RxVhB4b23iTFhCOdHFhxfbsxLxWf+ocvswJwR/9Wh/E//ddMi+wJGqUKV7VwoA==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.2" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "17.2.3", + "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "6.0.7", + "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + }, + "ZstdSharp.Port": { + "type": "Transitive", + "resolved": "0.6.2", + "contentHash": "jPao/LdUNLUz8rn3H1D8W7wQbZsRZM0iayvWI4xGejJg3XJHT56gcmYdgmCGPdJF1UEBqUjucCRrFB+4HbJsbw==" + }, + "monai.deploy.informaticsgateway.api": { + "type": "Project", + "dependencies": { + "Macross.Json.Extensions": "[3.0.0, )", + "Microsoft.EntityFrameworkCore.Abstractions": "[6.0.21, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )", + "Monai.Deploy.Messaging": "[1.0.0, )", + "Monai.Deploy.Storage": "[0.2.18, )", + "fo-dicom": "[5.1.1, )" + } + }, + "monai.deploy.informaticsgateway.common": { + "type": "Project", + "dependencies": { + "Ardalis.GuardClauses": "[4.1.1, )", + "System.IO.Abstractions": "[17.2.3, )" + } + }, + "monai.deploy.informaticsgateway.configuration": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Common": "[1.0.0, )" + } + }, + "monai.deploy.informaticsgateway.database.api": { + "type": "Project", + "dependencies": { + "Monai.Deploy.InformaticsGateway.Api": "[1.0.0, )", + "Monai.Deploy.InformaticsGateway.Configuration": "[1.0.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Shared/Test/InstanceGenerator.cs b/src/Shared/Test/InstanceGenerator.cs index 2287c18d6..954d738b7 100644 --- a/src/Shared/Test/InstanceGenerator.cs +++ b/src/Shared/Test/InstanceGenerator.cs @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 MONAI Consortium + * Copyright 2021-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * limitations under the License. */ -using System.IO; using System.IO.Abstractions; using FellowOakDicom; using FellowOakDicom.Network; @@ -65,7 +64,7 @@ public static byte[] GenerateDicomData( var dataset = GenerateDicomDataset(studyInstanceUid, seriesInstanceUid, ref sopInstanceUid); var dicomfile = new DicomFile(dataset); - using var ms = new MemoryStream(); + using var ms = new System.IO.MemoryStream(); dicomfile.Save(ms); return ms.ToArray(); } diff --git a/src/Shared/Test/TestStorageInfo.cs b/src/Shared/Test/TestStorageInfo.cs index b326c0715..a5dbc74b4 100644 --- a/src/Shared/Test/TestStorageInfo.cs +++ b/src/Shared/Test/TestStorageInfo.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ */ using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.Messaging.Events; namespace Monai.Deploy.InformaticsGateway.SharedTest; internal record TestStorageInfo : FileStorageMetadata { - public TestStorageInfo(string correlationsId, string identifier, string filePath, string fileExtension) + public TestStorageInfo(string correlationsId, string identifier, string filePath, string fileExtension, DataOrigin dataOrigin) : base(correlationsId, identifier) { File = new StorageObjectMetadata(fileExtension) @@ -28,6 +29,9 @@ public TestStorageInfo(string correlationsId, string identifier, string filePath UploadPath = filePath, TemporaryPath = filePath }; + DataOrigin.Source = dataOrigin.Source; + DataOrigin.Destination = dataOrigin.Destination; + DataOrigin.DataService = dataOrigin.DataService; } public override string DataTypeDirectoryName => "dir"; @@ -38,4 +42,4 @@ public void SetUploaded() { File.SetUploaded("test"); } -} +} \ No newline at end of file diff --git a/tests/Integration.Test/Common/Assertions.cs b/tests/Integration.Test/Common/Assertions.cs index 33787d549..09d7eeb5f 100644 --- a/tests/Integration.Test/Common/Assertions.cs +++ b/tests/Integration.Test/Common/Assertions.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ using FellowOakDicom; using FellowOakDicom.Serialization; using Minio; +using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Integration.Test.Drivers; using Monai.Deploy.Messaging.Events; @@ -48,10 +49,10 @@ public Assertions(Configurations configurations, InformaticsGatewayConfiguration _retryPolicy = Policy.Handle().WaitAndRetryAsync(retryCount: 5, sleepDurationProvider: _ => TimeSpan.FromMilliseconds(500)); } - internal async Task ShouldHaveUploadedDicomDataToMinio(IReadOnlyList messages, Dictionary fileHashes) + internal async Task ShouldHaveUploadedDicomDataToMinio(IReadOnlyList messages, Dictionary fileHashes, Action additionalChecks = null) { - Guard.Against.Null(messages); - Guard.Against.NullOrEmpty(fileHashes); + Guard.Against.Null(messages, nameof(messages)); + Guard.Against.NullOrEmpty(fileHashes, nameof(fileHashes)); var minioClient = GetMinioClient(); @@ -75,6 +76,12 @@ await _retryPolicy.ExecuteAsync(async () => memoryStream.Position = 0; var dicomFile = DicomFile.Open(memoryStream); dicomValidationKey = dicomFile.GenerateFileName(); + + if (additionalChecks is not null) + { + additionalChecks(dicomFile); + } + fileHashes.Should().ContainKey(dicomValidationKey).WhoseValue.Should().Be(dicomFile.CalculateHash()); }); await minioClient.GetObjectAsync(getObjectArgs); @@ -101,7 +108,7 @@ await _retryPolicy.ExecuteAsync(async () => internal async Task ShouldHaveUploadedFhirDataToMinio(IReadOnlyList messages, Dictionary fhirData) { - Guard.Against.Null(messages); + Guard.Against.Null(messages, nameof(messages)); var minioClient = GetMinioClient(); @@ -146,7 +153,7 @@ internal async Task ShouldHaveUploadedFhirDataToMinio(IReadOnlyList mes internal async Task ShouldHaveUploadedHl7ataToMinio(IReadOnlyList messages) { - Guard.Against.Null(messages); + Guard.Against.Null(messages, nameof(messages)); var minioClient = GetMinioClient(); @@ -188,10 +195,10 @@ internal async Task ShouldHaveUploadedHl7ataToMinio(IReadOnlyList messa } } - internal void ShouldHaveCorrectNumberOfWorkflowRequestMessages(DataProvider dataProvider, IReadOnlyList messages, int count) + internal void ShouldHaveCorrectNumberOfWorkflowRequestMessages(DataProvider dataProvider, DataService dataService, IReadOnlyList messages, int count) { - Guard.Against.Null(dataProvider); - Guard.Against.Null(messages); + Guard.Against.Null(dataProvider, nameof(dataProvider)); + Guard.Against.Null(messages, nameof(messages)); messages.Should().NotBeNullOrEmpty().And.HaveCount(count); foreach (var message in messages) @@ -213,13 +220,55 @@ internal void ShouldHaveCorrectNumberOfWorkflowRequestMessages(DataProvider data { request.Workflows.Should().Equal(dataProvider.Workflows); } + request.DataTrigger.Should().NotBeNull(); + request.DataTrigger.DataService.Should().Be(dataService); + request.DataTrigger.Source.Should().Be(dataProvider.Source); + request.DataTrigger.Destination.Should().Be(dataProvider.Destination); + + foreach (var dataOrigin in request.DataOrigins) + { + dataOrigin.DataService.Should().Be(dataService); + dataOrigin.Source.Should().Be(dataProvider.Source); + dataOrigin.Destination.Should().Be(dataProvider.Destination); + } + } + } + + internal void ShouldHaveCorrectNumberOfWorkflowRequestMessagesForFhirRequest(DataProvider dataProvider, DataService dataService, IReadOnlyList messages, int count) + { + Guard.Against.Null(dataProvider, nameof(dataProvider)); + Guard.Against.Null(messages, nameof(messages)); + + messages.Should().NotBeNullOrEmpty().And.HaveCount(count); + foreach (var message in messages) + { + message.ApplicationId.Should().Be(MessageBrokerConfiguration.InformaticsGatewayApplicationId); + var request = message.ConvertTo(); + request.Should().NotBeNull(); + request.FileCount.Should().Be(1); + + if (dataProvider.Workflows is not null) + { + request.Workflows.Should().Equal(dataProvider.Workflows); + } + request.DataTrigger.Should().NotBeNull(); + request.DataTrigger.DataService.Should().Be(dataService); + request.DataTrigger.Source.Should().Be(dataProvider.Source); + request.DataTrigger.Destination.Should().Be(FileStorageMetadata.IpAddress()); + + foreach (var dataOrigin in request.DataOrigins) + { + dataOrigin.DataService.Should().Be(dataService); + dataOrigin.Source.Should().Be(dataProvider.Source); + dataOrigin.Destination.Should().Be(dataProvider.Destination); + } } } internal void ShouldHaveCorrectNumberOfWorkflowRequestMessagesAndAcrRequest(DataProvider dataProvider, IReadOnlyList messages, int count) { - Guard.Against.Null(dataProvider); - Guard.Against.Null(messages); + Guard.Against.Null(dataProvider, nameof(dataProvider)); + Guard.Against.Null(messages, nameof(messages)); messages.Should().NotBeNullOrEmpty().And.HaveCount(count); @@ -230,13 +279,22 @@ internal void ShouldHaveCorrectNumberOfWorkflowRequestMessagesAndAcrRequest(Data request.Should().NotBeNull(); request.FileCount.Should().Be(dataProvider.DicomSpecs.FileCount); request.Workflows.Should().Equal(dataProvider.AcrRequest.Application.Id); + request.DataTrigger.Should().NotBeNull(); + request.DataTrigger.DataService.Should().Be(DataService.ACR); + request.DataTrigger.Source.Should().Be(dataProvider.AcrRequest.TransactionId); + + foreach (var dataOrigin in request.DataOrigins) + { + dataOrigin.DataService.Should().Be(DataService.DicomWeb); + dataOrigin.Source.Should().Be(dataProvider.AcrRequest.TransactionId); + } } } internal void ShouldHaveCorrectNumberOfWorkflowRequestMessagesAndHl7Messages(Hl7Messages hL7Specs, IReadOnlyList messages, int count) { - Guard.Against.Null(hL7Specs); - Guard.Against.Null(messages); + Guard.Against.Null(hL7Specs, nameof(hL7Specs)); + Guard.Against.Null(messages, nameof(messages)); messages.Should().NotBeNullOrEmpty().And.HaveCount(count); @@ -296,5 +354,52 @@ internal void ShoulddHaveCorrectNumberOfAckMessages(Dictionary r segment.Fields(9).Value.Should().Be("ACK"); } } + + internal async Task ShouldRestoreAllDicomMetaata(IReadOnlyList messages, Dictionary originalDicomFiles, params DicomTag[] dicomTags) + { + Guard.Against.Null(messages, nameof(messages)); + Guard.Against.NullOrEmpty(originalDicomFiles, nameof(originalDicomFiles)); + + var minioClient = GetMinioClient(); + + foreach (var message in messages) + { + var request = message.ConvertTo(); + foreach (var file in request.Payload) + { + await _retryPolicy.ExecuteAsync(async () => + { + var dicomValidationKey = string.Empty; + + _outputHelper.WriteLine($"Reading file from {request.Bucket} => {request.PayloadId}/{file.Path}."); + var getObjectArgs = new GetObjectArgs() + .WithBucket(request.Bucket) + .WithObject($"{request.PayloadId}/{file.Path}") + .WithCallbackStream((stream) => + { + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + memoryStream.Position = 0; + var dicomFile = DicomFile.Open(memoryStream); + dicomValidationKey = dicomFile.GenerateFileName(); + originalDicomFiles.Should().ContainKey(dicomValidationKey); + CompareDicomFiles(originalDicomFiles[dicomValidationKey], dicomFile, dicomTags); + }); + await minioClient.GetObjectAsync(getObjectArgs); + }); + } + } + } + + private void CompareDicomFiles(DicomFile left, DicomFile right, DicomTag[] dicomTags) + { + left.Should().NotBeNull(); + right.Should().NotBeNull(); + + foreach (var tag in dicomTags) + { + left.Dataset.GetString(tag).Should().Be(right.Dataset.GetString(tag)); + } + } } } diff --git a/tests/Integration.Test/Common/DataProvider.cs b/tests/Integration.Test/Common/DataProvider.cs index 069282473..cb609530b 100644 --- a/tests/Integration.Test/Common/DataProvider.cs +++ b/tests/Integration.Test/Common/DataProvider.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,128 @@ namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common { + internal static class DicomRandomDataProvider + { + private static readonly Random Random = new(); + private static readonly string AlphaNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static readonly string Numeric = "0123456789"; + + public static void InjectRandomData(this DicomDataset dataset, DicomTag tag) + { + var data = string.Empty; + switch (tag.DictionaryEntry.ValueRepresentations[0].Code) + { + case "IS": + data = RandomString(Numeric, 12); + dataset.AddOrUpdate(tag, Convert.ToInt32(data)); + return; + + case "UI": + data = DicomUIDGenerator.GenerateDerivedFromUUID().UID; + break; + + case "LO": + case "LT": + data = RandomString(AlphaNumeric, 64); + break; + + case "AE": + data = RandomString(AlphaNumeric, 16); + break; + + case "CS": + data = RandomString(Numeric, 16); + break; + + case "FL": + var bufferFl = new byte[4]; + Random.NextBytes(bufferFl); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferFl, 0).ToString()); + return; + + case "FD": + var bufferFd = new byte[8]; + Random.NextBytes(bufferFd); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferFd, 0).ToString()); + return; + + case "OD": + var bufferOd = new byte[8]; + Random.NextBytes(bufferOd); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferOd, 0)); + return; + + case "OF": + var bufferOf = new byte[4]; + Random.NextBytes(bufferOf); + dataset.AddOrUpdate(tag, BitConverter.ToSingle(bufferOf, 0)); + return; + + case "PN": + data = RandomString(AlphaNumeric, 64); + break; + + case "DA": + data = "20000101"; + break; + + case "DT": + data = "20000101000000"; + break; + + case "TM": + data = "000000"; + break; + + case "SH": + data = RandomString(Numeric, 16); + break; + + case "DS": + case "SL": + case "UL": + data = RandomString(Numeric, 4); + break; + + case "SS": + case "US": + data = RandomString(Numeric, 2); + break; + + case "OB": + case "OW": + var bufferBytes = new byte[4]; + Random.NextBytes(bufferBytes); + dataset.AddOrUpdate(tag, bufferBytes); + break; + + case "ST": + case "UN": + case "UT": + data = RandomString(AlphaNumeric, 1024); + break; + + case "AS": + data = $"{RandomString(Numeric, 3).PadLeft(3, '0')}Y"; + break; + } + dataset.AddOrUpdate(tag, data); + } + + public static string RandomString(string characterSet, int maxLength) + { + var length = Random.Next(1, maxLength); + var output = new char[length]; + + for (int i = 0; i < length; i++) + { + output[i] = characterSet[Random.Next(characterSet.Length)]; + } + + return new string(output); + } + } + internal class DataProvider { private readonly Configurations _configurations; @@ -41,6 +163,8 @@ internal class DataProvider public int ClientTimeout { get; internal set; } public int ClientAssociationPulseTime { get; internal set; } = 0; public int ClientSendOverAssociations { get; internal set; } = 1; + public string Source { get; internal set; } = string.Empty; + public string Destination { get; internal set; } = string.Empty; public DataProvider(Configurations configurations, ISpecFlowOutputHelper outputHelper) { @@ -53,7 +177,7 @@ public DataProvider(Configurations configurations, ISpecFlowOutputHelper outputH internal void GenerateDicomData(string modality, int studyCount, int? seriesPerStudy = null) { - Guard.Against.NullOrWhiteSpace(modality); + Guard.Against.NullOrWhiteSpace(modality, nameof(modality)); _outputHelper.WriteLine($"Generating {studyCount} {modality} study"); _configurations.StudySpecs.ContainsKey(modality).Should().BeTrue(); @@ -71,10 +195,21 @@ internal void GenerateDicomData(string modality, int studyCount, int? seriesPerS _outputHelper.WriteLine($"File specs: {DicomSpecs.StudyCount}, {DicomSpecs.SeriesPerStudyCount}, {DicomSpecs.InstancePerSeries}, {DicomSpecs.FileCount}"); } + internal void InjectRandomData(params DicomTag[] tags) + { + foreach (var dicomFile in DicomSpecs.Files.Values) + { + foreach (var tag in tags) + { + dicomFile.Dataset.InjectRandomData(tag); + } + } + } + internal void ReplaceGeneratedDicomDataWithHashes() { var dicomFileSize = new Dictionary(); - foreach (var dicomFile in DicomSpecs.Files) + foreach (var dicomFile in DicomSpecs.Files.Values) { var key = dicomFile.GenerateFileName(); dicomFileSize[key] = dicomFile.CalculateHash(); @@ -86,7 +221,7 @@ internal void ReplaceGeneratedDicomDataWithHashes() internal void GenerateAcrRequest(string requestType) { - Guard.Against.NullOrWhiteSpace(requestType); + Guard.Against.NullOrWhiteSpace(requestType, nameof(requestType)); var inferenceRequest = new InferenceRequest(); inferenceRequest.TransactionId = Guid.NewGuid().ToString(); @@ -105,12 +240,12 @@ internal void GenerateAcrRequest(string requestType) case "Patient": inferenceRequest.InputMetadata.Details.Type = InferenceRequestType.DicomPatientId; - inferenceRequest.InputMetadata.Details.PatientId = DicomSpecs.Files[0].Dataset.GetSingleValue(DicomTag.PatientID); + inferenceRequest.InputMetadata.Details.PatientId = DicomSpecs.Files.Values.First().Dataset.GetSingleValue(DicomTag.PatientID); break; case "AccessionNumber": inferenceRequest.InputMetadata.Details.Type = InferenceRequestType.AccessionNumber; - inferenceRequest.InputMetadata.Details.AccessionNumber = new List() { DicomSpecs.Files[0].Dataset.GetSingleValue(DicomTag.AccessionNumber) }; + inferenceRequest.InputMetadata.Details.AccessionNumber = new List() { DicomSpecs.Files.Values.First().Dataset.GetSingleValue(DicomTag.AccessionNumber) }; break; default: diff --git a/tests/Integration.Test/Common/DicomCEchoDataClient.cs b/tests/Integration.Test/Common/DicomCEchoDataClient.cs index b91a67725..49918c0e2 100644 --- a/tests/Integration.Test/Common/DicomCEchoDataClient.cs +++ b/tests/Integration.Test/Common/DicomCEchoDataClient.cs @@ -38,7 +38,7 @@ public DicomCEchoDataClient(Configurations configurations, InformaticsGatewayCon public async Task SendAsync(DataProvider dataProvider, params object[] args) { - Guard.Against.NullOrEmpty(args); + Guard.Against.NullOrEmpty(args, nameof(args)); var callingAeTitle = args[0].ToString(); var host = args[1].ToString(); diff --git a/tests/Integration.Test/Common/DicomCStoreDataClient.cs b/tests/Integration.Test/Common/DicomCStoreDataClient.cs index 5b4400860..bd010a8d0 100644 --- a/tests/Integration.Test/Common/DicomCStoreDataClient.cs +++ b/tests/Integration.Test/Common/DicomCStoreDataClient.cs @@ -43,7 +43,7 @@ public DicomCStoreDataClient(Configurations configurations, InformaticsGatewayCo public async Task SendAsync(DataProvider dataProvider, params object[] args) { - Guard.Against.NullOrEmpty(args); + Guard.Against.NullOrEmpty(args, nameof(args)); var callingAeTitle = args[0].ToString(); var host = args[1].ToString(); @@ -62,10 +62,10 @@ public async Task SendAsync(DataProvider dataProvider, params object[] args) var failureStatus = new List(); for (int i = 0; i < associations; i++) { - var files = dataProvider.DicomSpecs.Files.Skip(i * filesPerAssociations).Take(filesPerAssociations).ToList(); // .Take(20) + var files = dataProvider.DicomSpecs.Files.Values.Skip(i * filesPerAssociations).Take(filesPerAssociations).ToList(); // .Take(20) if (i + 1 == associations && dataProvider.DicomSpecs.Files.Count > (i + 1) * filesPerAssociations) { - files.AddRange(dataProvider.DicomSpecs.Files.Skip(i * filesPerAssociations)); + files.AddRange(dataProvider.DicomSpecs.Files.Values.Skip(i * filesPerAssociations)); } try diff --git a/tests/Integration.Test/Common/DicomDataSpecs.cs b/tests/Integration.Test/Common/DicomDataSpecs.cs index 9fa614187..b5db556e6 100644 --- a/tests/Integration.Test/Common/DicomDataSpecs.cs +++ b/tests/Integration.Test/Common/DicomDataSpecs.cs @@ -25,7 +25,7 @@ internal class DicomDataSpecs public int SeriesPerStudyCount { get; set; } public int InstancePerSeries { get; set; } public int FileCount { get; set; } - public List Files { get; set; } + public Dictionary Files { get; set; } = new Dictionary(); public Dictionary FileHashes { get; set; } = new Dictionary(); public int NumberOfExpectedRequests(string grouping) => grouping switch diff --git a/tests/Integration.Test/Common/DicomScp.cs b/tests/Integration.Test/Common/DicomScp.cs index 6858ca122..ea56624f3 100644 --- a/tests/Integration.Test/Common/DicomScp.cs +++ b/tests/Integration.Test/Common/DicomScp.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ using System.Text; using FellowOakDicom; -using FellowOakDicom.Log; using FellowOakDicom.Network; +using Microsoft.Extensions.Logging; +using Monai.Deploy.InformaticsGateway.Test.PlugIns; using TechTalk.SpecFlow.Infrastructure; namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common { - public class DicomScp : IDisposable { public readonly string FeatureScpAeTitle = "TEST-SCP"; @@ -31,8 +31,12 @@ public class DicomScp : IDisposable private readonly IDicomServer _server; private bool _disposedValue; - public Dictionary Instances { get; set; } = new Dictionary(); + public Dictionary> Instances { get; set; } = new Dictionary>(); + public Dictionary DicomFiles { get; set; } = new Dictionary(); public ISpecFlowOutputHelper OutputHelper { get; set; } + + public bool ClearFilesAndUseHashes { get; set; } = true; + public DicomScp(ISpecFlowOutputHelper outputHelper) { OutputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); @@ -53,7 +57,6 @@ protected virtual void Dispose(bool disposing) } } - public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method @@ -62,15 +65,13 @@ public void Dispose() } } - - internal class CStoreScp : DicomService, IDicomServiceProvider, IDicomCStoreProvider { private static readonly object SyncLock = new object(); internal static readonly string PayloadsRoot = "./payloads"; - public CStoreScp(INetworkStream stream, Encoding fallbackEncoding, ILogger log, DicomServiceDependencies dicomServiceDependencies) - : base(stream, fallbackEncoding, log, dicomServiceDependencies) + public CStoreScp(INetworkStream stream, Encoding fallbackEncoding, ILogger logger, DicomServiceDependencies dicomServiceDependencies) + : base(stream, fallbackEncoding, logger, dicomServiceDependencies) { } @@ -98,7 +99,17 @@ public Task OnCStoreRequestAsync(DicomCStoreRequest request var key = request.File.GenerateFileName(); lock (SyncLock) { - data.Instances.Add(key, request.File.CalculateHash()); + if (data.ClearFilesAndUseHashes) + { + var values = new List(); + data.Instances.Add(key, values); + values.Add(request.File.CalculateHash()); + values.Add(request.File.Dataset.GetSingleValueOrDefault(TestOutputDataPlugInModifyDicomFile.ExpectedTag, string.Empty)); + } + else + { + data.DicomFiles.Add(key, request.File); + } } data.OutputHelper.WriteLine("Instance received {0}", key); @@ -140,6 +151,7 @@ public Task OnReceiveAssociationRequestAsync(DicomAssociation association) return SendAssociationAcceptAsync(association); } - public void OnConnectionClosed(Exception exception) { } + public void OnConnectionClosed(Exception exception) + { } } } diff --git a/tests/Integration.Test/Common/DicomWebDataSink.cs b/tests/Integration.Test/Common/DicomWebDataSink.cs index dc7f2e8f2..07ad2ee76 100644 --- a/tests/Integration.Test/Common/DicomWebDataSink.cs +++ b/tests/Integration.Test/Common/DicomWebDataSink.cs @@ -49,8 +49,8 @@ public DicomWebDataClient(Configurations configurations, InformaticsGatewayConfi /// public async Task SendAsync(DataProvider dataProvider, params object[] args) { - Guard.Against.Null(dataProvider); - Guard.Against.Null(args); + Guard.Against.Null(dataProvider, nameof(dataProvider)); + Guard.Against.Null(args, nameof(args)); var dicomFileSpec = dataProvider.DicomSpecs; dicomFileSpec.Should().NotBeNull(); diff --git a/tests/Integration.Test/Common/FhirDataSink.cs b/tests/Integration.Test/Common/FhirDataSink.cs index 1157cfcf2..5db77db96 100644 --- a/tests/Integration.Test/Common/FhirDataSink.cs +++ b/tests/Integration.Test/Common/FhirDataSink.cs @@ -38,7 +38,7 @@ public FhirDataClient(Configurations configurations, InformaticsGatewayConfigura public async Task SendAsync(DataProvider dataProvider, params object[] args) { - Guard.Against.Null(dataProvider); + Guard.Against.Null(dataProvider, nameof(dataProvider)); var httpClient = HttpClientFactory.Create(); httpClient.BaseAddress = new Uri($"{_configurations.InformaticsGatewayOptions.ApiEndpoint}/fhir/"); httpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(dataProvider.FhirSpecs.MediaType)); diff --git a/tests/Integration.Test/Common/Hl7DataSink.cs b/tests/Integration.Test/Common/Hl7DataSink.cs index 607d960f0..0c340e686 100644 --- a/tests/Integration.Test/Common/Hl7DataSink.cs +++ b/tests/Integration.Test/Common/Hl7DataSink.cs @@ -39,8 +39,8 @@ public Hl7DataClient(Configurations configurations, InformaticsGatewayConfigurat public async Task SendAsync(DataProvider dataProvider, params object[] args) { - Guard.Against.Null(dataProvider); - Guard.Against.NullOrEmpty(args); + Guard.Against.Null(dataProvider, nameof(dataProvider)); + Guard.Against.NullOrEmpty(args, nameof(args)); var batch = (bool)args[0]; diff --git a/tests/Integration.Test/Common/MinioDataSink.cs b/tests/Integration.Test/Common/MinioDataSink.cs index 7d90f2ebd..dde07d4a1 100644 --- a/tests/Integration.Test/Common/MinioDataSink.cs +++ b/tests/Integration.Test/Common/MinioDataSink.cs @@ -50,7 +50,7 @@ await _retryPolicy.ExecuteAsync(async () => _outputHelper.WriteLine($"Uploading {dataProvider.DicomSpecs.FileCount} files to MinIO..."); - foreach (var file in dataProvider.DicomSpecs.Files) + foreach (var file in dataProvider.DicomSpecs.Files.Values) { var filename = file.GenerateFileName(); var stream = new MemoryStream(); diff --git a/tests/Integration.Test/Common/RabbitConnectionFactory.cs b/tests/Integration.Test/Common/RabbitConnectionFactory.cs index ad357e62f..36e6881c2 100644 --- a/tests/Integration.Test/Common/RabbitConnectionFactory.cs +++ b/tests/Integration.Test/Common/RabbitConnectionFactory.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,10 +80,10 @@ public static void PurgeAllQueues(MessageBrokerConfiguration configuration) { PurgeQueue(configuration, configuration.Topics.WorkflowRequest); PurgeQueue(configuration, configuration.Topics.ExportComplete); - PurgeQueue(configuration, configuration.Topics.ExportRequestPrefix); // TODO + PurgeQueue(configuration, configuration.Topics.ExportRequestPrefix); PurgeQueue(configuration, $"{configuration.Topics.WorkflowRequest}-dead-letter"); PurgeQueue(configuration, $"{configuration.Topics.ExportComplete}-dead-letter"); - PurgeQueue(configuration, $"{configuration.Topics.ExportRequestPrefix}-dead-letter"); // TODO + PurgeQueue(configuration, $"{configuration.Topics.ExportRequestPrefix}-dead-letter"); } } } diff --git a/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs b/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs index 727c19cf3..539d95476 100644 --- a/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs +++ b/tests/Integration.Test/Drivers/DicomInstanceGenerator.cs @@ -103,7 +103,7 @@ public DicomDataSpecs Generate(string patientId, int studiesPerPatient, int seri studySpec.InstanceMax.Should().BeGreaterThan(0); var instancesPerSeries = _random.Next(studySpec.InstanceMin, studySpec.InstanceMax); - var files = new List(); + var files = new Dictionary(); var studyInstanceUids = new List(); DicomFile dicomFile = null; @@ -120,7 +120,7 @@ public DicomDataSpecs Generate(string patientId, int studiesPerPatient, int seri { var size = _random.NextLong(studySpec.SizeMinBytes, studySpec.SizeMaxBytes); dicomFile = generator.GenerateNewInstance(size); - files.Add(dicomFile); + files.Add(dicomFile.GenerateFileName(), dicomFile); } } _outputHelper.WriteLine("DICOM Instance: PID={0}, STUDY={1}", diff --git a/tests/Integration.Test/Drivers/EfDataProvider.cs b/tests/Integration.Test/Drivers/EfDataProvider.cs index 2a8be5757..cda1b3b47 100644 --- a/tests/Integration.Test/Drivers/EfDataProvider.cs +++ b/tests/Integration.Test/Drivers/EfDataProvider.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ public EfDataProvider(ISpecFlowOutputHelper outputHelper, Configurations configu private string ConvertToFullPath(string connectionString) { - Guard.Against.NullOrWhiteSpace(connectionString); + Guard.Against.NullOrWhiteSpace(connectionString, nameof(connectionString)); string absolute = Path.GetFullPath("./"); return connectionString.Replace("=./", $"={absolute}"); @@ -61,6 +61,7 @@ public void ClearAllData() DumpAndClear("DestinationApplicationEntities", _dbContext.DestinationApplicationEntities.ToList()); DumpAndClear("SourceApplicationEntities", _dbContext.SourceApplicationEntities.ToList()); DumpAndClear("MonaiApplicationEntities", _dbContext.MonaiApplicationEntities.ToList()); + DumpAndClear("VirtualApplicationEntities", _dbContext.VirtualApplicationEntities.ToList()); DumpAndClear("Payloads", _dbContext.Payloads.ToList()); DumpAndClear("InferenceRequests", _dbContext.InferenceRequests.ToList()); DumpAndClear("StorageMetadataWrapperEntities", _dbContext.StorageMetadataWrapperEntities.ToList()); diff --git a/tests/Integration.Test/Drivers/MongoDBDataProvider.cs b/tests/Integration.Test/Drivers/MongoDBDataProvider.cs index 9ef02d425..2c86182b7 100644 --- a/tests/Integration.Test/Drivers/MongoDBDataProvider.cs +++ b/tests/Integration.Test/Drivers/MongoDBDataProvider.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2022-2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ public class MongoDBDataProvider : IDatabaseDataProvider private readonly IMongoCollection _sourceApplicationEntityCollection; private readonly IMongoCollection _destinationApplicationEntityCollection; private readonly IMongoCollection _monaiApplicationEntityCollection; + private readonly IMongoCollection _virtualApplicationEntityCollection; public MongoDBDataProvider(ISpecFlowOutputHelper outputHelper, Configurations configuration, string connectionString, string databaseName) { @@ -63,6 +64,7 @@ public MongoDBDataProvider(ISpecFlowOutputHelper outputHelper, Configurations co _sourceApplicationEntityCollection = _database.GetCollection(nameof(SourceApplicationEntity)); _destinationApplicationEntityCollection = _database.GetCollection(nameof(DestinationApplicationEntity)); _monaiApplicationEntityCollection = _database.GetCollection(nameof(MonaiApplicationEntity)); + _virtualApplicationEntityCollection = _database.GetCollection(nameof(VirtualApplicationEntity)); } public void ClearAllData() @@ -74,6 +76,7 @@ public void ClearAllData() DumpClear(_sourceApplicationEntityCollection); DumpClear(_destinationApplicationEntityCollection); DumpClear(_monaiApplicationEntityCollection); + DumpClear(_virtualApplicationEntityCollection); _outputHelper.WriteLine("All data removed from the database."); } diff --git a/tests/Integration.Test/Drivers/RabbitMqConsumer.cs b/tests/Integration.Test/Drivers/RabbitMqConsumer.cs index e82f53d1a..70b34da43 100644 --- a/tests/Integration.Test/Drivers/RabbitMqConsumer.cs +++ b/tests/Integration.Test/Drivers/RabbitMqConsumer.cs @@ -48,12 +48,13 @@ public RabbitMqConsumer(RabbitMQMessageSubscriberService subscriberService, stri subscriberService.SubscribeAsync( queueName, queueName, - async (eventArgs) => + (eventArgs) => { _outputHelper.WriteLine($"Message received from queue {queueName} for {queueName}."); _messages.Add(eventArgs.Message); subscriberService.Acknowledge(eventArgs.Message); _outputHelper.WriteLine($"{DateTime.UtcNow} - {queueName} message received with correlation ID={eventArgs.Message.CorrelationId}, delivery tag={eventArgs.Message.DeliveryTag}"); + return Task.CompletedTask; }); } diff --git a/tests/Integration.Test/Features/DicomDimseScp.feature b/tests/Integration.Test/Features/DicomDimseScp.feature index 1c4116a16..7741e62ed 100644 --- a/tests/Integration.Test/Features/DicomDimseScp.feature +++ b/tests/Integration.Test/Features/DicomDimseScp.feature @@ -44,7 +44,7 @@ Feature: DICOM DIMSE SCP Services When a C-STORE-RQ is sent to 'Informatics Gateway' with AET '' from 'TEST-RUNNER' Then a successful response should be received And workflow requests sent to message broker - And studies are uploaded to storage service + And studies are uploaded to storage service with data input plugins Examples: | modality | count | aet | timeout | diff --git a/tests/Integration.Test/Features/DicomWebStow.feature b/tests/Integration.Test/Features/DicomWebStow.feature index f49224294..2d892cd4b 100644 --- a/tests/Integration.Test/Features/DicomWebStow.feature +++ b/tests/Integration.Test/Features/DicomWebStow.feature @@ -24,8 +24,8 @@ Feature: DICOMweb STOW-RS Service Scenario: Triggers a new workflow request via DICOMWeb STOW-RS Given studies with 'stow_none' grouping When the studies are uploaded to the DICOMWeb STOW-RS service at '/dicomweb/' - Then 1 workflow requests sent to message broker - And studies are uploaded to storage service + Then 1 workflow requests received from message broker + And studies are uploaded to storage service with data input plugins Examples: | modality | count | | MR | 1 | @@ -35,8 +35,8 @@ Feature: DICOMweb STOW-RS Service Scenario: Triggers a new workflow with given study instance UID request via DICOMWeb STOW-RS Given studies with 'stow_study' grouping When the studies are uploaded to the DICOMWeb STOW-RS service at '/dicomweb/' with StudyInstanceUid - Then 1 workflow requests sent to message broker - And studies are uploaded to storage service + Then 1 workflow requests received from message broker + And studies are uploaded to storage service with data input plugins Examples: | modality | count | | MR | 1 | @@ -47,8 +47,8 @@ Feature: DICOMweb STOW-RS Service Given studies with 'stow_none' grouping And a workflow named 'MyWorkflow' When the studies are uploaded to the DICOMWeb STOW-RS service at '/dicomweb/' - Then 1 workflow requests sent to message broker - And studies are uploaded to storage service + Then 1 workflow requests received from message broker + And studies are uploaded to storage service with data input plugins Examples: | modality | count | | MR | 2 | @@ -59,9 +59,35 @@ Feature: DICOMweb STOW-RS Service Given studies with 'stow_study' grouping And a workflow named 'MyWorkflow' When the studies are uploaded to the DICOMWeb STOW-RS service at '/dicomweb/' with StudyInstanceUid - Then 1 workflow requests sent to message broker - And studies are uploaded to storage service + Then 1 workflow requests received from message broker + And studies are uploaded to storage service with data input plugins Examples: | modality | count | | MR | 2 | | US | 1 | + + @messaging_workflow_request @messaging + Scenario: Triggers a new workflow request via DICOMWeb STOW-RS with Virtual AET + Given a VirtualAE 'cool' + And studies with 'stow_none' grouping + When the studies are uploaded to the DICOMWeb STOW-RS service at '/dicomweb/vae/cool/' without overriding workflows + Then 1 workflow requests received from message broker + And studies are uploaded to storage service with data input VAE plugin + Examples: + | modality | count | + | MR | 1 | + | MG | 2 | + + + @messaging_workflow_request @messaging + Scenario: Triggers a new workflow with given study instance UID request via DICOMWeb STOW-RSw ith Virtual AET + Given a VirtualAE 'awesome' + And studies with 'stow_study' grouping + And a workflow named 'MyWorkflow' + When the studies are uploaded to the DICOMWeb STOW-RS service at '/dicomweb/vae/awesome/' with StudyInstanceUid + Then 1 workflow requests received from message broker + And studies are uploaded to storage service with data input VAE plugin + Examples: + | modality | count | + | MR | 1 | + | MG | 2 | diff --git a/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature b/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature new file mode 100644 index 000000000..bfb40fd59 --- /dev/null +++ b/tests/Integration.Test/Features/RemoteAppExecutionPlugIn.feature @@ -0,0 +1,27 @@ +# Copyright 2022 MONAI Consortium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# @ignored + +Feature: Remote App Execution Plug-in + +This feature tests the Remote App Execution plug-ins for de-identifying and +re-identifying data sent and received by the MIG respectively. + + + @messaging_workflow_request @messaging + Scenario: End-to-end test of plug-ins + Given a study that is exported to the test host + When the study is received and sent back to Informatics Gateway + Then ensure the original study and the received study are the same diff --git a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj index eea628e76..4565805b5 100644 --- a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj +++ b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj @@ -1,5 +1,5 @@