changeset 1228:7035bf7ce6bc

python SCP callbacks update
author Alain Mazy <am@orthanc.team>
date Wed, 26 Nov 2025 15:55:48 +0100
parents 4b3bfabe3212
children e15a7861fdcd
files Sphinx/source/plugins/python.rst Sphinx/source/plugins/python/dicom-find-move-scp.py Sphinx/source/plugins/python/dicom-find-scp.py Sphinx/source/plugins/python/dicom-move-scp.py Sphinx/source/plugins/python/storage-commitment-default.py Sphinx/source/plugins/python/worklist.py
diffstat 6 files changed, 134 insertions(+), 61 deletions(-) [+]
line wrap: on
line diff
--- a/Sphinx/source/plugins/python.rst	Wed Nov 26 11:04:26 2025 +0100
+++ b/Sphinx/source/plugins/python.rst	Wed Nov 26 15:55:48 2025 +0100
@@ -702,12 +702,11 @@
 Starting with release 3.2 of the Python plugin, it is possible to
 replace the C-FIND SCP and C-MOVE SCP of Orthanc by a Python
 script. This feature can notably be used to create a custom DICOM
-proxy. Here is a minimal example:
+proxy. Here is a minimal example for a C-Find handler:
 
-.. literalinclude:: python/dicom-find-move-scp.py
+.. literalinclude:: python/dicom-find-scp.py
                     :language: python
 
-
 .. highlight:: text
   
 In this sample, the C-FIND SCP will send one single answer that
@@ -730,38 +729,19 @@
 Orthanc using ``orthanc.RestApiPost()``, in order to query the content
 a remote modality through a second C-FIND SCU request (this time
 issued by Orthanc as a SCU).
+
+Here is a minimal example for a C-Move handler:
+
+.. literalinclude:: python/dicom-move-scp.py
+                    :language: python
+
   
 The C-MOVE SCP can be invoked as follows::
   
   $ movescu localhost 4242 -aem TARGET -aec SOURCE -aet MOVESCU -S -k QueryRetrieveLevel=IMAGE -k StudyInstanceUID=1.2.3.4
 
-The C-MOVE request above would print the following information in the
-Orthanc logs::
 
-  W0610 18:30:36.840865 PluginsManager.cpp:168] C-MOVE request to be handled in Python: {
-      "AccessionNumber": "", 
-      "Level": "INSTANCE", 
-      "OriginatorAET": "MOVESCU", 
-      "OriginatorID": 1, 
-      "PatientID": "", 
-      "SOPInstanceUID": "", 
-      "SeriesInstanceUID": "", 
-      "SourceAET": "SOURCE", 
-      "StudyInstanceUID": "1.2.3.4", 
-      "TargetAET": "TARGET"
-  }
-
-It is now up to your Python callback to process the C-MOVE SCU request,
-for instance by calling the route ``/modalities/{...}/store`` in the
-:ref:`REST API <rest-store-scu>` of Orthanc using
-``orthanc.RestApiPost()``. It is highly advised to create a Python
-thread to handle the request, in order to avoid blocking Orthanc as
-much as possible.
-
-
-**Note:** In version 4.2, we have introduced a new version of the C-MOVE SCP 
-handler that can be registered through ``orthanc.RegisterMoveCallback2(CreateMoveCallback, GetMoveSizeCallback, ApplyMoveCallback, FreeMoveCallback)``.
-This `DICOM to DICOMweb proxy sample project <https://github.com/orthanc-team/dicom-dicomweb-proxy/blob/main/proxy.py>`__ demonstrates how it can be used.
+**Note:** A full sample is available in this `DICOM to DICOMweb proxy sample project <https://github.com/orthanc-team/dicom-dicomweb-proxy/blob/main/proxy.py>`__.
 
 
 .. _python_worklists:
--- a/Sphinx/source/plugins/python/dicom-find-move-scp.py	Wed Nov 26 11:04:26 2025 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-import json
-import orthanc
-import pprint
-
-def OnFind(answers, query, issuerAet, calledAet):
-    print('Received incoming C-FIND request from %s:' % issuerAet)
-
-    answer = {}
-    for i in range(query.GetFindQuerySize()):
-        print('  %s (%04x,%04x) = [%s]' % (query.GetFindQueryTagName(i),
-                                           query.GetFindQueryTagGroup(i),
-                                           query.GetFindQueryTagElement(i),
-                                           query.GetFindQueryValue(i)))
-        answer[query.GetFindQueryTagName(i)] = ('HELLO%d-%s' % (i, query.GetFindQueryValue(i)))
-
-    answers.FindAddAnswer(orthanc.CreateDicom(
-        json.dumps(answer), None, orthanc.CreateDicomFlags.NONE))
-
-def OnMove(**request):
-    orthanc.LogWarning('C-MOVE request to be handled in Python: %s' %
-                       json.dumps(request, indent = 4, sort_keys = True))
-
-    # To indicate a failure in the processing, one can raise an exception:
-    #   raise Exception('Cannot handle C-MOVE')
-
-orthanc.RegisterFindCallback(OnFind)
-orthanc.RegisterMoveCallback(OnMove)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Sphinx/source/plugins/python/dicom-find-scp.py	Wed Nov 26 15:55:48 2025 +0100
@@ -0,0 +1,25 @@
+import json
+import orthanc
+
+
+def OnFind(answers, query, connection):  # new from v 7.0: issuerAet and calledAet are available from the connection object
+    print('Received incoming C-FIND request from %s %s %s:' % (connection.GetConnectionRemoteAet(), connection.GetConnectionRemoteIp(), connection.GetConnectionCalledAet()))
+
+    # old prototype still available
+    # def OnFindLegacy(answers, query, issuerAet, calledAet):
+    #     print('Received incoming C-FIND request from %s:' % issuerAet)
+
+    answer = {}
+    for i in range(query.GetFindQuerySize()):
+        print('  %s (%04x,%04x) = [%s]' % (query.GetFindQueryTagName(i),
+                                           query.GetFindQueryTagGroup(i),
+                                           query.GetFindQueryTagElement(i),
+                                           query.GetFindQueryValue(i)))
+        answer[query.GetFindQueryTagName(i)] = ('HELLO%d-%s' % (i, query.GetFindQueryValue(i)))
+
+    answers.FindAddAnswer(orthanc.CreateDicom(
+        json.dumps(answer), None, orthanc.CreateDicomFlags.NONE))
+
+orthanc.RegisterFindCallback2(OnFind)        # new from v 7.0
+#orthanc.RegisterFindCallback(OnFindLegacy)  # old version, still available
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Sphinx/source/plugins/python/dicom-move-scp.py	Wed Nov 26 15:55:48 2025 +0100
@@ -0,0 +1,82 @@
+import json
+import orthanc
+import pprint
+
+def OnMoveBasic(**request):
+    orthanc.LogWarning('C-MOVE request to be handled in Python: %s' %
+                       json.dumps(request, indent = 4, sort_keys = True))
+
+    # The C-MOVE request above would print the following information in the
+    # Orthanc logs::
+
+    #   W0610 18:30:36.840865 PluginsManager.cpp:168] C-MOVE request to be handled in Python: {
+    #       "AccessionNumber": "", 
+    #       "Level": "INSTANCE", 
+    #       "OriginatorAET": "MOVESCU", 
+    #       "OriginatorID": 1, 
+    #       "PatientID": "", 
+    #       "SOPInstanceUID": "", 
+    #       "SeriesInstanceUID": "", 
+    #       "SourceAET": "SOURCE", 
+    #       "StudyInstanceUID": "1.2.3.4", 
+    #       "TargetAET": "TARGET"
+    #   }
+
+    # To indicate a failure in the processing, one can raise an exception:
+    #   raise Exception('Cannot handle C-MOVE')
+
+    # It is now up to your Python callback to process the C-MOVE SCU request,
+    # for instance by calling the route /modalities/{...}/store.
+
+
+# More advanced Move driver, providing progress reporting to the MOVE SCU originator and
+# providing more information about the DicomConnection.  
+# For a full sample, see https://github.com/orthanc-team/dicom-dicomweb-proxy/blob/main/proxy.py
+class MoveDriver:
+
+    def __init__(self, request, connection) -> None:
+        self.request = request  # dictionnary containing the C-MOVE request e.g: {
+        #       "AccessionNumber": "", 
+        #       "Level": "INSTANCE", 
+        #       "OriginatorID": 1, 
+        #       "PatientID": "", 
+        #       "SOPInstanceUID": "", 
+        #       "SeriesInstanceUID": "", 
+        #       "StudyInstanceUID": "1.2.3.4",
+        #       "TargetAET": "TARGET"
+        #   }
+
+        # connection.GetConnectionCalledAet()  is equivalent to request["SourceAET"] in older versions of the callback
+        # connection.GetConnectionRemoteAet()  is equivalent to request["OriginatorAET"] in older versions of the callback
+        # connection.GetConnectionRemoteIp()   is new in v 7.0
+
+        self.instances_ids_to_transfer = [] # TODO: build a list of instances to transfer from the query
+        self.instance_counter = 0
+
+
+def CreateMoveCallback(connection, **request):  # from v 7.0; to use with orthanc.RegisterMoveCallback3()
+    # simply create the move driver object now and return it to Orthanc
+    orthanc.LogInfo("CreateMoveCallback")
+    driver = MoveDriver(request=request, connection=connection)
+    return driver
+
+def GetMoveSizeCallback(driver: MoveDriver):
+    # query the remote server to list and count the instances to retrieve
+    orthanc.LogInfo("GetMoveSizeCallback")
+    return len(driver.instances_ids_to_transfer)
+
+def ApplyMoveCallback(driver: MoveDriver):
+    # move one instance at a time
+    orthanc.LogInfo("ApplyMoveCallback")
+    instance_id = driver.instances_ids_to_transfer[driver.instance_counter]
+    driver.instance_counter += 1
+    # TODO store the instance in the destination
+    return orthanc.ErrorCode.SUCCESS
+
+def FreeMoveCallback(driver):
+    # free the resources that have been allocated by the move driver - if any
+    orthanc.LogInfo("FreeMoveCallback")
+    
+
+orthanc.RegisterMoveCallback3(CreateMoveCallback, GetMoveSizeCallback, ApplyMoveCallback, FreeMoveCallback)
+# orthanc.RegisterMoveCallback(OnMoveBasic)
--- a/Sphinx/source/plugins/python/storage-commitment-default.py	Wed Nov 26 11:04:26 2025 +0100
+++ b/Sphinx/source/plugins/python/storage-commitment-default.py	Wed Nov 26 15:55:48 2025 +0100
@@ -3,11 +3,18 @@
 
 # this plugins provides the same behavior as the default Orthanc implementation
 
-def StorageCommitmentScpCallback(jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet):
+# new callback format from v 7.0; to use with RegisterStorageCommitmentScpCallback2
+def StorageCommitmentScpCallback(jobId, transactionUid, sopClassUids, sopInstanceUids, connection):
     # At the beginning of a Storage Commitment operation, you can build a custom data structure
     # that will be provided as the "data" argument in the StorageCommitmentLookup
     return None
 
+# old prototype to use with RegisterStorageCommitmentScpCallback
+# def StorageCommitmentScpCallback(jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet):
+#     # At the beginning of a Storage Commitment operation, you can build a custom data structure
+#     # that will be provided as the "data" argument in the StorageCommitmentLookup
+#     return None
+
 
 # Reference: `StorageCommitmentScpJob::Lookup` in `OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp`
 def StorageCommitmentLookup(sopClassUid, sopInstanceUid, data):
@@ -31,4 +38,5 @@
 
     return reason
 
-orthanc.RegisterStorageCommitmentScpCallback(StorageCommitmentScpCallback, StorageCommitmentLookup)
\ No newline at end of file
+orthanc.RegisterStorageCommitmentScpCallback2(StorageCommitmentScpCallback, StorageCommitmentLookup)  # from v 7.0
+# orthanc.RegisterStorageCommitmentScpCallback(StorageCommitmentScpCallback, StorageCommitmentLookup)
\ No newline at end of file
--- a/Sphinx/source/plugins/python/worklist.py	Wed Nov 26 11:04:26 2025 +0100
+++ b/Sphinx/source/plugins/python/worklist.py	Wed Nov 26 15:55:48 2025 +0100
@@ -6,8 +6,12 @@
 # https://orthanc.uclouvain.be/hg/orthanc/file/Orthanc-1.11.0/OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase
 WORKLIST_DIR = '/tmp/WorklistsDatabase'
 
-def OnWorklist(answers, query, issuerAet, calledAet):
-    print('Received incoming C-FIND worklist request from %s:' % issuerAet)
+def OnWorklist(answers, query, connection):  # new from v 7.0: issuerAet and calledAet are available from the connection object
+    print('Received incoming C-FIND worklist request from %s %s %s:' % (connection.GetConnectionRemoteAet(), connection.GetConnectionRemoteIp(), connection.GetConnectionCalledAet()))
+
+    # old prototype still available
+    # def OnWorklist(answers, query, issuerAet, calledAet):
+    #     print('Received incoming C-FIND worklist request from %s:' % issuerAet)
 
     # Get a memory buffer containing the DICOM instance
     dicom = query.WorklistGetDicomQuery()
@@ -30,4 +34,5 @@
                     orthanc.LogWarning('Matching worklist: %s' % path)
                     answers.WorklistAddAnswer(query, content)
 
-orthanc.RegisterWorklistCallback(OnWorklist)
+orthanc.RegisterWorklistCallback2(OnWorklist)  # new from v 7.0
+# orthanc.RegisterWorklistCallback(OnWorklist)