changeset 302:7ffc9e968b3e

SCP callbacks: worklists + storage commitment
author Alain Mazy <am@orthanc.team>
date Wed, 26 Nov 2025 15:54:16 +0100
parents 4210556d96ab
children a3af4f9f6b99 fbb298dd7cda
files CodeAnalysis/CustomFunctions.json NEWS Sources/DicomScpCallbacks.cpp Sources/StorageCommitmentScpCallback.cpp
diffstat 4 files changed, 231 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/CodeAnalysis/CustomFunctions.json	Wed Nov 26 08:39:29 2025 +0100
+++ b/CodeAnalysis/CustomFunctions.json	Wed Nov 26 15:54:16 2025 +0100
@@ -207,7 +207,7 @@
     ],
     "return_sdk_type" : "void",
     "since_sdk" : [ 1, 12, 10 ],
-    "sdk_functions" : [ "OrthancPluginRegisterFindCallback" ]
+    "sdk_functions" : [ "OrthancPluginRegisterFindCallback2" ]
   },
 
   {
@@ -277,7 +277,8 @@
       }
     ],
     "return_sdk_type" : "void",
-    "since_sdk" : [ 1, 12, 10 ]
+    "since_sdk" : [ 1, 1, 0 ],
+    "sdk_functions" : [ "OrthancPluginRegisterMoveCallback" ]
   },
 
   {
@@ -324,7 +325,8 @@
       }
     ],
     "return_sdk_type" : "void",
-    "since_sdk" : [ 1, 12, 10 ]
+    "since_sdk" : [ 1, 12, 10 ],
+    "sdk_functions" : [ "OrthancPluginRegisterMoveCallback2" ]
   },
 
   {
@@ -351,6 +353,30 @@
   },
 
   {
+    "comment" : "New in release 7.0",
+    "short_name" : "RegisterWorklistCallback2",
+    "implementation" : "RegisterWorklistCallback2",
+    "documentation" : {
+      "description" : [ "Register a callback to handle modality worklists requests (v2)." ],
+      "args" : {
+        "callback" : "The callback function."
+      }
+    },
+    "args" : [
+      {
+        "sdk_name" : "callback",
+        "sdk_type" : "Callable",
+        "callable_type" : "WorklistCallback2",
+        "callable_protocol_args" : "answers: WorklistAnswers, query: WorklistQuery, connection: DicomConnection",
+        "callable_protocol_return" : "None"
+      }
+    ],
+    "return_sdk_type" : "void",
+    "sdk_functions" : [ "OrthancPluginRegisterWorklistCallback2" ],
+    "since_sdk" : [ 1, 12, 10 ]
+  },
+
+  {
     "comment" : "New in release 3.3",
     "short_name" : "RegisterStorageArea",
     "implementation" : "RegisterStorageArea",
@@ -467,6 +493,38 @@
   },
 
   {
+    "comment" : "New in release 7.0",
+    "short_name" : "RegisterStorageCommitmentScpCallback2",
+    "implementation" : "RegisterStorageCommitmentScpCallback2",
+    "documentation" : {
+      "description" : [ "Register a callback to handle incoming requests to the storage commitment SCP (v2)." ],
+      "args" : {
+        "callback" : "Main callback that creates the a driver to handle an incoming storage commitment request.",
+        "lookup" : "Callback function to get the status of one DICOM instance."
+      }
+    },
+    "args" : [
+      {
+        "sdk_name" : "callback",
+        "sdk_type" : "Callable",
+        "callable_type" : "StorageCommitmentScpCallback2",
+        "callable_protocol_args" : "job_id: str, transaction_uid: str, sop_class_uids: list[str], sop_instance_uids: list[str], connection: DicomConnection",
+        "callable_protocol_return" : "object",  "comment" : "This is the newly created storage commitment driver."
+      },
+      {
+        "sdk_name" : "lookup",
+        "sdk_type" : "Callable",
+        "callable_type" : "StorageCommitmentLookup",
+        "callable_protocol_args" : "sop_class_uid: str, sop_instance_uid: str, driver: object",
+        "callable_protocol_return" : "StorageCommitmentFailureReason"
+      }
+    ],
+    "return_sdk_type" : "void",
+    "sdk_functions" : [ "OrthancPluginRegisterStorageCommitmentScpCallback2" ],
+    "since_sdk" : [ 1, 12, 10 ]
+  },
+
+  {
     "comment" : "New in release 6.0",
     "short_name" : "GetKeyValue",
     "implementation" : "GetKeyValue",
--- a/NEWS	Wed Nov 26 08:39:29 2025 +0100
+++ b/NEWS	Wed Nov 26 15:54:16 2025 +0100
@@ -7,8 +7,9 @@
 * Added new SCP callbacks:
   - RegisterFindCallback2
   - RegisterMoveCallback3
-  - TODO: worklist + storage commitment
-  
+  - RegisterWorklistCallback2
+  - RegisterStorageCommitmentScpCallback2
+
 
 Version 6.0 (2025-08-12)
 ========================
--- a/Sources/DicomScpCallbacks.cpp	Wed Nov 26 08:39:29 2025 +0100
+++ b/Sources/DicomScpCallbacks.cpp	Wed Nov 26 15:54:16 2025 +0100
@@ -846,7 +846,58 @@
   }
 }
 
-   
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10)
+OrthancPluginErrorCode WorklistCallback2(OrthancPluginWorklistAnswers* answers,
+                                         const OrthancPluginWorklistQuery* query,
+                                         const OrthancPluginDicomConnection* connection)
+{
+  try
+  {
+    PythonLock lock;
+
+    PyObject *pAnswers, *pQuery, *pConnection;
+    
+    {
+      PythonObject args(lock, PyTuple_New(2));
+      PyTuple_SetItem(args.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) answers));
+      PyTuple_SetItem(args.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */));
+      pAnswers = PyObject_CallObject((PyObject *) GetOrthancPluginWorklistAnswersType(), args.GetPyObject());
+    }
+    
+    {
+      PythonObject args(lock, PyTuple_New(2));
+      PyTuple_SetItem(args.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) query));
+      PyTuple_SetItem(args.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */));
+      pQuery = PyObject_CallObject((PyObject *) GetOrthancPluginWorklistQueryType(), args.GetPyObject());
+    }
+    
+    {
+      PythonObject argsConnection(lock, PyTuple_New(2));
+      PyTuple_SetItem(argsConnection.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) connection));
+      PyTuple_SetItem(argsConnection.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */));
+      pConnection = PyObject_CallObject((PyObject*) GetOrthancPluginDicomConnectionType(), argsConnection.GetPyObject());
+    }
+
+    {
+      PythonObject args(lock, PyTuple_New(3));
+      PyTuple_SetItem(args.GetPyObject(), 0, pAnswers);
+      PyTuple_SetItem(args.GetPyObject(), 1, pQuery);
+      PyTuple_SetItem(args.GetPyObject(), 2, pConnection);
+
+      assert(worklistScpCallback_ != NULL);
+      PythonObject result(lock, PyObject_CallObject(worklistScpCallback_, args.GetPyObject()));
+    }
+
+    return lock.CheckCallbackSuccess("Python C-FIND SCP for worklist callback (v2)");
+  }
+  catch (OrthancPlugins::PluginException& e)
+  {
+    return e.GetErrorCode();
+  }
+}
+#endif
+
 PyObject* RegisterFindCallback(PyObject* module, PyObject* args)
 {
   // The GIL is locked at this point (no need to create "PythonLock")
@@ -967,6 +1018,25 @@
     registration, args, worklistScpCallback_, "Python C-FIND SCP for worklist callback");
 }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10)
+PyObject* RegisterWorklistCallback2(PyObject* module, PyObject* args)
+{
+  // The GIL is locked at this point (no need to create "PythonLock")
+
+  class Registration : public ICallbackRegistration
+  {
+  public:
+    virtual void Register() ORTHANC_OVERRIDE
+    {
+      OrthancPluginRegisterWorklistCallback2(OrthancPlugins::GetGlobalContext(), WorklistCallback2);
+    }
+  };
+
+  Registration registration;
+  return ICallbackRegistration::Apply(
+    registration, args, worklistScpCallback_, "Python C-FIND SCP for worklist callback (v2)");
+}
+#endif
 
 void FinalizeDicomScpCallbacks()
 {
--- a/Sources/StorageCommitmentScpCallback.cpp	Wed Nov 26 08:39:29 2025 +0100
+++ b/Sources/StorageCommitmentScpCallback.cpp	Wed Nov 26 15:54:16 2025 +0100
@@ -32,6 +32,7 @@
 #include "ICallbackRegistration.h"
 #include "PythonString.h"
 
+#include <sdk.h>
 
 static PyObject*   storageCommitmentScpCallback_ = NULL;
 static PyObject*   storageCommitmentLookupCallback_ = NULL;
@@ -103,6 +104,73 @@
   return OrthancPluginErrorCode_Success;
 }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10)
+static OrthancPluginErrorCode StorageCommitmentSCPCallback2(
+  void**              handler /* out */,
+  const char*         jobId,
+  const char*         transactionUid,
+  const char* const*  sopClassUids,
+  const char* const*  sopInstanceUids,
+  uint32_t            countInstances,
+  const OrthancPluginDicomConnection* connection)
+{
+  try
+  {
+    PythonLock lock;
+
+    PythonObject args(lock, PyTuple_New(5));
+    {
+      PythonString str(lock, jobId);
+      PyTuple_SetItem(args.GetPyObject(), 0, str.Release());
+    }
+    {
+      PythonString str(lock, transactionUid);
+      PyTuple_SetItem(args.GetPyObject(), 1, str.Release());
+    }
+    {
+      PythonObject sopClassUidList(lock, PyList_New(countInstances));
+      for (uint32_t i = 0; i < countInstances; i++)
+      {
+        PythonString str(lock, sopClassUids[i]);
+        PyList_SetItem(sopClassUidList.GetPyObject(), i, str.Release());
+      }
+      PyTuple_SetItem(args.GetPyObject(), 2, sopClassUidList.Release());
+      PythonObject sopInstanceUidList(lock, PyList_New(countInstances));
+      for (uint32_t i = 0; i < countInstances; i++)
+      {
+        PythonString str(lock, sopInstanceUids[i]);
+        PyList_SetItem(sopInstanceUidList.GetPyObject(), i, str.Release());
+      }
+      PyTuple_SetItem(args.GetPyObject(), 3, sopInstanceUidList.Release());
+    }
+    {
+      PythonObject argsConnection(lock, PyTuple_New(2));
+      PyTuple_SetItem(argsConnection.GetPyObject(), 0, PyLong_FromSsize_t((intptr_t) connection));
+      PyTuple_SetItem(argsConnection.GetPyObject(), 1, PyBool_FromLong(true /* borrowed, don't destruct */));
+      PyObject *pConnection = PyObject_CallObject((PyObject*) GetOrthancPluginDicomConnectionType(), argsConnection.GetPyObject());
+  
+      PyTuple_SetItem(args.GetPyObject(), 4, pConnection);
+    }
+
+    PythonObject result(lock, PyObject_CallObject(storageCommitmentScpCallback_, args.GetPyObject()));
+    *handler = result.Release();
+
+    std::string traceback;
+    if (lock.HasErrorOccurred(traceback))
+    {
+      ORTHANC_PLUGINS_LOG_ERROR("Error in the Python storage commitment SCP callback (v2), traceback:\n" + traceback);
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+  catch (OrthancPlugins::PluginException& e)
+  {
+    ORTHANC_PLUGINS_LOG_ERROR("Error in the Python storage commitment SCP callback (v2): " +
+                              std::string(e.What(OrthancPlugins::GetGlobalContext())));
+  }
+  return OrthancPluginErrorCode_Success;
+}
+#endif
+
 static OrthancPluginErrorCode StorageCommitmentLookupCallback(
   OrthancPluginStorageCommitmentFailureReason* target /* out */,
   void*                                        handler,
@@ -185,6 +253,34 @@
   }
 }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 10)
+PyObject* RegisterStorageCommitmentScpCallback2(PyObject* module, PyObject* args)
+{
+  // The GIL is locked at this point (no need to create "PythonLock")
+
+  class Registration : public ICallbackRegistration
+  {
+  public:
+    virtual void Register() ORTHANC_OVERRIDE
+    {
+      OrthancPluginRegisterStorageCommitmentScpCallback2(
+        OrthancPlugins::GetGlobalContext(),
+        StorageCommitmentSCPCallback2,
+        StorageCommitmentDestructor,
+        StorageCommitmentLookupCallback);
+    }
+  };
+
+  {
+    Registration registration;
+    return ICallbackRegistration::Apply2(registration, args,
+      storageCommitmentScpCallback_,
+      storageCommitmentLookupCallback_,
+      "Python storage commitment SCP & Lookup callback (v2)");
+  }
+}
+#endif
+
 void FinalizeStorageCommitmentScpCallback()
 {
   ICallbackRegistration::Unregister(storageCommitmentScpCallback_);