Skip to content

Commit 358f400

Browse files
authored
APIs to update DICOM source and destination (#197)
* gh-195 APIs to update DICOM source and destination * Use ICollectionFIxture to share SCP listener context Signed-off-by: Victor Chang <[email protected]>
1 parent 566ba8b commit 358f400

20 files changed

+822
-84
lines changed

docs/api/rest/config.md

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,16 @@ Response Content Type: JSON - [MonaiApplicationEntity](xref:Monai.Deploy.Informa
144144

145145
```bash
146146
curl --location --request POST 'http://localhost:5000/config/ae/' \
147-
--header 'Content-Type: application/json' \
148-
--data-raw '{
149-
"name": "breast-tumor",
150-
"aeTitle": "BREASTV1",
151-
"timeout": 5,
152-
"workflows": [
153-
"3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e"
154-
]
155-
}
156-
}'
147+
--header 'Content-Type: application/json' \
148+
--data-raw '{
149+
"name": "breast-tumor",
150+
"aeTitle": "BREASTV1",
151+
"timeout": 5,
152+
"workflows": [
153+
"3f6a08a1-0dea-44e9-ab82-1ff1adf43a8e"
154+
]
155+
}
156+
}'
157157
```
158158

159159
### Example Response
@@ -310,12 +310,56 @@ Response Content Type: JSON - [SourceApplicationEntity](xref:Monai.Deploy.Inform
310310

311311
```bash
312312
curl --location --request POST 'http://localhost:5000/config/source' \
313-
--header 'Content-Type: application/json' \
314-
--data-raw '{
315-
"name": "USEAST",
316-
"hostIp": "10.20.3.4",
317-
"aeTitle": "PACSUSEAST"
318-
}'
313+
--header 'Content-Type: application/json' \
314+
--data-raw '{
315+
"name": "USEAST",
316+
"hostIp": "10.20.3.4",
317+
"aeTitle": "PACSUSEAST"
318+
}'
319+
```
320+
321+
### Example Response
322+
323+
```json
324+
{
325+
"name": "USEAST",
326+
"aeTitle": "PACSUSEAST",
327+
"hostIp": "10.20.3.4"
328+
}
329+
```
330+
331+
---
332+
333+
## PUT /config/source
334+
335+
Updates an existing calling (source) AE Title.
336+
337+
### Parameters
338+
339+
See the [SourceApplicationEntity](xref:Monai.Deploy.InformaticsGateway.Api.SourceApplicationEntity)
340+
class definition for details.
341+
342+
### Responses
343+
344+
Response Content Type: JSON - [SourceApplicationEntity](xref:Monai.Deploy.InformaticsGateway.Api.SourceApplicationEntity).
345+
346+
| Code | Description |
347+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
348+
| 200 | AE Title updated successfully. |
349+
| 400 | Validation error. The response will be a [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) object with details of the validation errors . |
350+
| 404 | DICOM source cannot be found. |
351+
| 500 | Server error. The response will be a [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) object with server error details. |
352+
353+
### Example Request
354+
355+
```bash
356+
curl --location --request PUT 'http://localhost:5000/config/source' \
357+
--header 'Content-Type: application/json' \
358+
--data-raw '{
359+
"name": "USEAST",
360+
"hostIp": "10.20.3.4",
361+
"aeTitle": "PACSUSEAST"
362+
}'
319363
```
320364

321365
### Example Response
@@ -479,6 +523,52 @@ curl --location --request DELETE 'http://localhost:5000/config/destination/cecho
479523

480524
---
481525

526+
## PUT /config/destination
527+
528+
Updates an existing DICOM destination.
529+
530+
### Parameters
531+
532+
See the [DestinationApplicationEntity](xref:Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity)
533+
class definition for details.
534+
535+
### Responses
536+
537+
Response Content Type: JSON - [DestinationApplicationEntity](xref:Monai.Deploy.InformaticsGateway.Api.DestinationApplicationEntity).
538+
539+
| Code | Description |
540+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
541+
| 200 | DICOM destination updated successfully. |
542+
| 400 | Validation error. The response will be a [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) object with details of the validation errors . |
543+
| 404 | DICOM destination cannot be found. |
544+
| 500 | Server error. The response will be a [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) object with server error details. |
545+
546+
### Example Request
547+
548+
```bash
549+
curl --location --request PUT 'http://localhost:5000/config/destination' \
550+
--header 'Content-Type: application/json' \
551+
--data-raw '{
552+
"name": "USEAST",
553+
"hostIp": "10.20.3.4",
554+
"port": 104,
555+
"aeTitle": "PACSUSEAST"
556+
}'
557+
```
558+
559+
### Example Response
560+
561+
```json
562+
{
563+
"port": 104,
564+
"name": "USEAST",
565+
"aeTitle": "PACSUSEAST",
566+
"hostIp": "10.20.3.4"
567+
}
568+
```
569+
570+
---
571+
482572
## POST /config/destination
483573

484574
Adds a new DICOM destination AET for exporting results to.
@@ -502,13 +592,13 @@ Response Content Type: JSON - [DestinationApplicationEntity](xref:Monai.Deploy.I
502592

503593
```bash
504594
curl --location --request POST 'http://localhost:5000/config/destination' \
505-
--header 'Content-Type: application/json' \
506-
--data-raw '{
507-
"name": "USEAST",
508-
"hostIp": "10.20.3.4",
509-
"port": 104,
510-
"aeTitle": "PACSUSEAST"
511-
}'
595+
--header 'Content-Type: application/json' \
596+
--data-raw '{
597+
"name": "USEAST",
598+
"hostIp": "10.20.3.4",
599+
"port": 104,
600+
"aeTitle": "PACSUSEAST"
601+
}'
512602
```
513603

514604
### Example Response

src/CLI/Commands/DestinationCommand.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public DestinationCommand() : base("dst", "Configure DICOM destinations")
4141
AddAlias("destination");
4242

4343
SetupAddDestinationCommand();
44+
SetupEditDestinationCommand();
4445
SetupRemoveDestinationCommand();
4546
SetupListDestinationCommand();
4647
SetupCEchoCommand();
@@ -67,6 +68,23 @@ private void SetupListDestinationCommand()
6768
listCommand.Handler = CommandHandler.Create<DestinationApplicationEntity, IHost, bool, CancellationToken>(ListDestinationHandlerAsync);
6869
}
6970

71+
private void SetupEditDestinationCommand()
72+
{
73+
var addCommand = new Command("update", "Update a new DICOM destination");
74+
AddCommand(addCommand);
75+
76+
var nameOption = new Option<string>(new string[] { "--name", "-n" }, "Name of the DICOM destination") { IsRequired = false };
77+
addCommand.AddOption(nameOption);
78+
var aeTitleOption = new Option<string>(new string[] { "--aetitle", "-a" }, "AE Title of the DICOM destination") { IsRequired = true };
79+
addCommand.AddOption(aeTitleOption);
80+
var hostOption = new Option<string>(new string[] { "--host-ip", "-h" }, "Host or IP address of the DICOM destination") { IsRequired = true };
81+
addCommand.AddOption(hostOption);
82+
var portOption = new Option<int>(new string[] { "--port", "-p" }, "Listening port of the DICOM destination") { IsRequired = true };
83+
addCommand.AddOption(portOption);
84+
85+
addCommand.Handler = CommandHandler.Create<DestinationApplicationEntity, IHost, bool, CancellationToken>(EditDestinationHandlerAsync);
86+
}
87+
7088
private void SetupRemoveDestinationCommand()
7189
{
7290
var removeCommand = new Command("rm", "Remove a DICOM destination");
@@ -234,6 +252,44 @@ private async Task<int> RemoveDestinationHandlerAsync(string name, IHost host, b
234252
return ExitCodes.Success;
235253
}
236254

255+
private async Task<int> EditDestinationHandlerAsync(DestinationApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationToken)
256+
{
257+
Guard.Against.Null(entity, nameof(entity));
258+
Guard.Against.Null(host, nameof(host));
259+
260+
LogVerbose(verbose, host, "Configuring services...");
261+
var configService = host.Services.GetRequiredService<IConfigurationService>();
262+
var client = host.Services.GetRequiredService<IInformaticsGatewayClient>();
263+
var logger = CreateLogger<DestinationCommand>(host);
264+
265+
Guard.Against.Null(logger, nameof(logger), "Logger is unavailable.");
266+
Guard.Against.Null(configService, nameof(configService), "Configuration service is unavailable.");
267+
Guard.Against.Null(client, nameof(client), $"{Strings.ApplicationName} client is unavailable.");
268+
269+
try
270+
{
271+
CheckConfiguration(configService);
272+
client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
273+
274+
LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
275+
LogVerbose(verbose, host, $"Updating DICOM destination {entity.AeTitle}...");
276+
var result = await client.DicomDestinations.Update(entity, cancellationToken).ConfigureAwait(false);
277+
278+
logger.DicomDestinationCreated(result.Name, result.AeTitle, result.HostIp, result.Port);
279+
}
280+
catch (ConfigurationException ex)
281+
{
282+
logger.ConfigurationException(ex.Message);
283+
return ExitCodes.Config_NotConfigured;
284+
}
285+
catch (Exception ex)
286+
{
287+
logger.ErrorUpdatingDicomDestination(entity.AeTitle, ex.Message);
288+
return ExitCodes.DestinationAe_ErrorUpdate;
289+
}
290+
return ExitCodes.Success;
291+
}
292+
237293
private async Task<int> AddDestinationHandlerAsync(DestinationApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationToken)
238294
{
239295
Guard.Against.Null(entity, nameof(entity));

src/CLI/Commands/SourceCommand.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ public SourceCommand() : base("src", "Configure DICOM sources")
4040
AddAlias("source");
4141

4242
SetupAddSourceCommand();
43+
SetupUpdateSourceCommand();
4344
SetupRemoveSourceCommand();
4445
SetupListSourceCommand();
4546
}
4647

48+
4749
private void SetupListSourceCommand()
4850
{
4951
var listCommand = new Command("ls", "List all DICOM sources");
@@ -79,6 +81,20 @@ private void SetupAddSourceCommand()
7981

8082
addCommand.Handler = CommandHandler.Create<SourceApplicationEntity, IHost, bool, CancellationToken>(AddSourceHandlerAsync);
8183
}
84+
private void SetupUpdateSourceCommand()
85+
{
86+
var addCommand = new Command("update", "Update a new DICOM source");
87+
AddCommand(addCommand);
88+
89+
var nameOption = new Option<string>(new string[] { "--name", "-n" }, "Name of the DICOM source") { IsRequired = false };
90+
addCommand.AddOption(nameOption);
91+
var aeTitleOption = new Option<string>(new string[] { "--aetitle", "-a" }, "AE Title of the DICOM source") { IsRequired = true };
92+
addCommand.AddOption(aeTitleOption);
93+
var hostOption = new Option<string>(new string[] { "--host-ip", "-h" }, "Host or IP address of the DICOM source") { IsRequired = true };
94+
addCommand.AddOption(hostOption);
95+
96+
addCommand.Handler = CommandHandler.Create<SourceApplicationEntity, IHost, bool, CancellationToken>(UpdateSourceHandlerAsync);
97+
}
8298

8399
private async Task<int> ListSourceHandlerAsync(SourceApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationTokena)
84100
{
@@ -217,5 +233,41 @@ private async Task<int> AddSourceHandlerAsync(SourceApplicationEntity entity, IH
217233
}
218234
return ExitCodes.Success;
219235
}
236+
private async Task<int> UpdateSourceHandlerAsync(SourceApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationTokena)
237+
{
238+
Guard.Against.Null(entity, nameof(entity));
239+
Guard.Against.Null(host, nameof(host));
240+
241+
LogVerbose(verbose, host, "Configuring services...");
242+
var configService = host.Services.GetRequiredService<IConfigurationService>();
243+
var client = host.Services.GetRequiredService<IInformaticsGatewayClient>();
244+
var logger = CreateLogger<SourceCommand>(host);
245+
246+
Guard.Against.Null(logger, nameof(logger), "Logger is unavailable.");
247+
Guard.Against.Null(configService, nameof(configService), "Configuration service is unavailable.");
248+
Guard.Against.Null(client, nameof(client), $"{Strings.ApplicationName} client is unavailable.");
249+
250+
try
251+
{
252+
CheckConfiguration(configService);
253+
client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
254+
LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
255+
LogVerbose(verbose, host, $"Updating DICOM source {entity.AeTitle}...");
256+
var result = await client.DicomSources.Update(entity, cancellationTokena).ConfigureAwait(false);
257+
258+
logger.DicomSourceUpdated(result.Name, result.AeTitle, result.HostIp);
259+
}
260+
catch (ConfigurationException ex)
261+
{
262+
logger.ConfigurationException(ex.Message);
263+
return ExitCodes.Config_NotConfigured;
264+
}
265+
catch (Exception ex)
266+
{
267+
logger.ErrorUpdatingDicomSource(entity.AeTitle, ex.Message);
268+
return ExitCodes.SourceAe_ErrorUpdate;
269+
}
270+
return ExitCodes.Success;
271+
}
220272
}
221273
}

src/CLI/ExitCodes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ public static class ExitCodes
3333
public const int DestinationAe_ErrorDelete = 301;
3434
public const int DestinationAe_ErrorCreate = 302;
3535
public const int DestinationAe_ErrorCEcho = 303;
36+
public const int DestinationAe_ErrorUpdate = 304;
3637

3738
public const int SourceAe_ErrorList = 400;
3839
public const int SourceAe_ErrorDelete = 401;
3940
public const int SourceAe_ErrorCreate = 402;
41+
public const int SourceAe_ErrorUpdate = 403;
4042

4143
public const int Restart_Cancelled = 500;
4244
public const int Restart_Error = 501;

src/CLI/Logging/Log.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,18 @@ public static partial class Log
169169
[LoggerMessage(EventId = 30055, Level = LogLevel.Critical, Message = "C-ECHO to {name} failed: {error}.")]
170170
public static partial void ErrorCEchogDicomDestination(this ILogger logger, string name, string error);
171171

172+
[LoggerMessage(EventId = 30056, Level = LogLevel.Information, Message = "DICOM destination updated:\r\n\tName: {name}\r\n\tAE Title: {aeTitle}\r\n\tHost/IP Address: {hostIp}\r\n\tPort: {port}")]
173+
public static partial void DicomDestinationUpdated(this ILogger logger, string name, string aeTitle, string hostIp, int port);
174+
175+
[LoggerMessage(EventId = 30057, Level = LogLevel.Critical, Message = "Error updating DICOM destination {aeTitle}: {message}")]
176+
public static partial void ErrorUpdatingDicomDestination(this ILogger logger, string aeTitle, string message);
177+
178+
[LoggerMessage(EventId = 30058, Level = LogLevel.Information, Message = "DICOM source updated:\r\n\tName: {name}\r\n\tAE Title: {aeTitle}\r\n\tHost/IP Address: {hostIp}")]
179+
public static partial void DicomSourceUpdated(this ILogger logger, string name, string aeTitle, string hostIp);
180+
181+
[LoggerMessage(EventId = 30059, Level = LogLevel.Critical, Message = "Error updating DICOM source {aeTitle}: {message}")]
182+
public static partial void ErrorUpdatingDicomSource(this ILogger logger, string aeTitle, string message);
183+
172184
// Docker Runner
173185
[LoggerMessage(EventId = 31000, Level = LogLevel.Debug, Message = "Checking for existing {applicationName} ({version}) containers...")]
174186
public static partial void CheckingExistingAppContainer(this ILogger logger, string applicationName, string version);

0 commit comments

Comments
 (0)