Mercurial > hg > orthanc-book
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)
