# HG changeset patch # User Sebastien Jodogne # Date 1623399848 -7200 # Node ID ba2403ebd4b7318f1b179018746ab09e0321348c # Parent a589668768d7ca10a23a8c7c257b994387d40e88 moving python samples in separate files (3) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python.rst --- a/Sphinx/source/plugins/python.rst Fri Jun 11 10:07:12 2021 +0200 +++ b/Sphinx/source/plugins/python.rst Fri Jun 11 10:24:08 2021 +0200 @@ -351,35 +351,11 @@ Scheduling a task for periodic execution ........................................ -.. highlight:: python - The following Python script will periodically (every second) run the -function ``Hello()`` thanks to the ``threading`` module:: - - import orthanc - import threading - - TIMER = None +function ``Hello()`` thanks to the ``threading`` module: - def Hello(): - global TIMER - TIMER = None - orthanc.LogWarning("In Hello()") - # Do stuff... - TIMER = threading.Timer(1, Hello) # Re-schedule after 1 second - TIMER.start() - - def OnChange(changeType, level, resource): - if changeType == orthanc.ChangeType.ORTHANC_STARTED: - orthanc.LogWarning("Starting the scheduler") - Hello() - - elif changeType == orthanc.ChangeType.ORTHANC_STOPPED: - if TIMER != None: - orthanc.LogWarning("Stopping the scheduler") - TIMER.cancel() - - orthanc.RegisterOnChangeCallback(OnChange) +.. literalinclude:: python/periodic-execution.py + :language: python .. _python-metadata: @@ -399,83 +375,11 @@ tags. Filtering metadata requires a linear search over all the matching resources, which induces a cost in the performance. -.. highlight:: python - Nevertheless, here is a full sample Python script that overwrites the -``/tools/find`` route in order to give access to metadata:: - - import json - import orthanc - import re - - # Get the path in the REST API to the given resource that was returned - # by a call to "/tools/find" - def GetPath(resource): - if resource['Type'] == 'Patient': - return '/patients/%s' % resource['ID'] - elif resource['Type'] == 'Study': - return '/studies/%s' % resource['ID'] - elif resource['Type'] == 'Series': - return '/series/%s' % resource['ID'] - elif resource['Type'] == 'Instance': - return '/instances/%s' % resource['ID'] - else: - raise Exception('Unknown resource level') - - def FindWithMetadata(output, uri, **request): - # The "/tools/find" route expects a POST method - if request['method'] != 'POST': - output.SendMethodNotAllowed('POST') - else: - # Parse the query provided by the user, and backup the "Expand" field - query = json.loads(request['body']) - - if 'Expand' in query: - originalExpand = query['Expand'] - else: - originalExpand = False +``/tools/find`` route in order to give access to metadata: - # Call the core "/tools/find" route - query['Expand'] = True - answers = orthanc.RestApiPost('/tools/find', json.dumps(query)) - - # Loop over the matching resources - filteredAnswers = [] - for answer in json.loads(answers): - try: - # Read the metadata that is associated with the resource - metadata = json.loads(orthanc.RestApiGet('%s/metadata?expand' % GetPath(answer))) - - # Check whether the metadata matches the regular expressions - # that were provided in the "Metadata" field of the user request - isMetadataMatch = True - if 'Metadata' in query: - for (name, pattern) in query['Metadata'].items(): - if name in metadata: - value = metadata[name] - else: - value = '' - - if re.match(pattern, value) == None: - isMetadataMatch = False - break - - # If all the metadata matches the provided regular - # expressions, add the resource to the filtered answers - if isMetadataMatch: - if originalExpand: - answer['Metadata'] = metadata - filteredAnswers.append(answer) - else: - filteredAnswers.append(answer['ID']) - except: - # The resource was deleted since the call to "/tools/find" - pass - - # Return the filtered answers in the JSON format - output.AnswerBuffer(json.dumps(filteredAnswers, indent = 3), 'application/json') - - orthanc.RegisterRestCallback('/tools/find', FindWithMetadata) +.. literalinclude:: python/filtering-metadata.py + :language: python **Warning:** In the sample above, the filtering of the metadata is @@ -498,54 +402,15 @@ Implementing basic paging ......................... -.. highlight:: python - As explained in the FAQ, the :ref:`Orthanc Explorer interface is low-level `, and is not adapted for end-users. One common need is to implement paging of studies, which calls for server-side sorting of studies. This can be done using the following sample Python plugin that registers a new route -``/sort-studies`` in the REST API of Orthanc:: - - import json - import orthanc - - def GetStudyDate(study): - if 'StudyDate' in study['MainDicomTags']: - return study['MainDicomTags']['StudyDate'] - else: - return '' - - def SortStudiesByDate(output, uri, **request): - if request['method'] == 'GET': - # Retrieve all the studies - studies = json.loads(orthanc.RestApiGet('/studies?expand')) - - # Sort the studies according to the "StudyDate" DICOM tag - studies = sorted(studies, key = GetStudyDate) +``/sort-studies`` in the REST API of Orthanc: - # Read the limit/offset arguments provided by the user - offset = 0 - if 'offset' in request['get']: - offset = int(request['get']['offset']) - - limit = 0 - if 'limit' in request['get']: - limit = int(request['get']['limit']) - - # Truncate the list of studies - if limit == 0: - studies = studies[offset : ] - else: - studies = studies[offset : offset + limit] - - # Return the truncated list of studies - output.AnswerBuffer(json.dumps(studies), 'application/json') - else: - output.SendMethodNotAllowed('GET') - - orthanc.RegisterRestCallback('/sort-studies', SortStudiesByDate) - +.. literalinclude:: python/paging.py + :language: python .. highlight:: bash @@ -573,41 +438,12 @@ Creating a Microsoft Excel report ................................. -.. highlight:: python - As Orthanc plugins have access to any installed Python module, it is very easy to implement a server-side plugin that generates a report in -the Microsoft Excel ``.xls`` format. Here is a working example:: +the Microsoft Excel ``.xls`` format. Here is a working example: - import StringIO - import json - import orthanc - import xlwt - - def CreateExcelReport(output, uri, **request): - if request['method'] != 'GET' : - output.SendMethodNotAllowed('GET') - else: - # Create an Excel writer - excel = xlwt.Workbook() - sheet = excel.add_sheet('Studies') - - # Loop over the studies stored in Orthanc - row = 0 - studies = orthanc.RestApiGet('/studies?expand') - for study in json.loads(studies): - sheet.write(row, 0, study['PatientMainDicomTags'].get('PatientID')) - sheet.write(row, 1, study['PatientMainDicomTags'].get('PatientName')) - sheet.write(row, 2, study['MainDicomTags'].get('StudyDescription')) - row += 1 - - # Serialize the Excel workbook to a string, and return it to the caller - # https://stackoverflow.com/a/15649139/881731 - b = StringIO.StringIO() - excel.save(b) - output.AnswerBuffer(b.getvalue(), 'application/vnd.ms-excel') - - orthanc.RegisterRestCallback('/report.xls', CreateExcelReport) +.. literalinclude:: python/excel.py + :language: python If opening the ``http://localhost:8042/report.xls`` URI, this Python will generate a workbook with one sheet that contains the list of @@ -620,20 +456,12 @@ Forbid or allow access to REST resources (authorization, new in 3.0) .................................................................... -.. highlight:: python - The following Python script installs a callback that is triggered -whenever the HTTP server of Orthanc is accessed:: +whenever the HTTP server of Orthanc is accessed: - import orthanc - import pprint +.. literalinclude:: python/authorization-1.py + :language: python - def Filter(uri, **request): - print('User trying to access URI: %s' % uri) - pprint.pprint(request) - return True # False to forbid access - - orthanc.RegisterIncomingHttpRequestFilter(Filter) If access is not granted, the ``Filter`` callback must return ``False``. As a consequence, the HTTP status code would be set to @@ -646,47 +474,19 @@ callback that is available in :ref:`Lua scripts `. Thanks to Python, it is extremely easy to call remote Web services for -authorization. Here is an example using the ``requests`` library:: - - import json - import orthanc - import requests +authorization. Here is an example using the ``requests`` library: - def Filter(uri, **request): - body = { - 'uri' : uri, - 'headers' : request['headers'] - } - r = requests.post('http://localhost:8000/authorize', - data = json.dumps(body)) - return r.json() ['granted'] # Must be a Boolean - - orthanc.RegisterIncomingHttpRequestFilter(Filter) +.. literalinclude:: python/authorization-2.py + :language: python .. highlight:: javascript This filter could be used together with the following Web service implemented using `Node.js -`__:: - - const http = require('http'); +`__: - const requestListener = function(req, res) { - let body = ''; - req.on('data', function(chunk) { - body += chunk; - }); - req.on('end', function() { - console.log(JSON.parse(body)); - var answer = { - 'granted' : false // Forbid access - }; - res.writeHead(200); - res.end(JSON.stringify(answer)); - }); - } - - http.createServer(requestListener).listen(8000); +.. literalinclude:: python/authorization-node-service.js + :language: javascript .. _python_create_dicom: @@ -694,36 +494,12 @@ Creating DICOM instances (new in 3.2) ..................................... -.. highlight:: python - The following sample Python script will write on the disk a new DICOM instance including the traditional Lena sample image, and will decode -the single frame of this DICOM instance:: +the single frame of this DICOM instance: - import json - import orthanc - - def OnChange(changeType, level, resource): - if changeType == orthanc.ChangeType.ORTHANC_STARTED: - tags = { - 'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.1', - 'PatientID' : 'HELLO', - 'PatientName' : 'WORLD', - } - - with open('Lena.png', 'rb') as f: - img = orthanc.UncompressImage(f.read(), orthanc.ImageFormat.PNG) - - s = orthanc.CreateDicom(json.dumps(tags), img, orthanc.CreateDicomFlags.GENERATE_IDENTIFIERS) - - with open('/tmp/sample.dcm', 'wb') as f: - f.write(s) - - dicom = orthanc.CreateDicomInstance(s) - frame = dicom.GetInstanceDecodedFrame(0) - print('Size of the frame: %dx%d' % (frame.GetImageWidth(), frame.GetImageHeight())) - - orthanc.RegisterOnChangeCallback(OnChange) +.. literalinclude:: python/create-dicom.py + :language: python .. _python_dicom_scp: @@ -731,37 +507,14 @@ Handling DICOM SCP requests (new in 3.2) ........................................ -.. highlight:: python - 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: - 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)) - - orthanc.RegisterFindCallback(OnFind) - orthanc.RegisterMoveCallback(OnMove) +.. literalinclude:: python/dicom-find-move-scp.py + :language: python + .. highlight:: text @@ -954,8 +707,6 @@ Slave processes and the "orthanc" module ........................................ -.. highlight:: python - Very importantly, pay attention to the fact that **only the "master" Python interpreter has access to the Orthanc SDK**. The "slave" processes have no access to the ``orthanc`` module. diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/authorization-1.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/authorization-1.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,9 @@ +import orthanc +import pprint + +def Filter(uri, **request): + print('User trying to access URI: %s' % uri) + pprint.pprint(request) + return True # False to forbid access + +orthanc.RegisterIncomingHttpRequestFilter(Filter) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/authorization-2.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/authorization-2.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,14 @@ +import json +import orthanc +import requests + +def Filter(uri, **request): + body = { + 'uri' : uri, + 'headers' : request['headers'] + } + r = requests.post('http://localhost:8000/authorize', + data = json.dumps(body)) + return r.json() ['granted'] # Must be a Boolean + +orthanc.RegisterIncomingHttpRequestFilter(Filter) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/authorization-node-service.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/authorization-node-service.js Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,18 @@ +const http = require('http'); + +const requestListener = function(req, res) { + let body = ''; + req.on('data', function(chunk) { + body += chunk; + }); + req.on('end', function() { + console.log(JSON.parse(body)); + var answer = { + 'granted' : false // Forbid access + }; + res.writeHead(200); + res.end(JSON.stringify(answer)); + }); +} + +http.createServer(requestListener).listen(8000); diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/create-dicom.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/create-dicom.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,24 @@ +import json +import orthanc + +def OnChange(changeType, level, resource): + if changeType == orthanc.ChangeType.ORTHANC_STARTED: + tags = { + 'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.1', + 'PatientID' : 'HELLO', + 'PatientName' : 'WORLD', + } + + with open('Lena.png', 'rb') as f: + img = orthanc.UncompressImage(f.read(), orthanc.ImageFormat.PNG) + + s = orthanc.CreateDicom(json.dumps(tags), img, orthanc.CreateDicomFlags.GENERATE_IDENTIFIERS) + + with open('/tmp/sample.dcm', 'wb') as f: + f.write(s) + + dicom = orthanc.CreateDicomInstance(s) + frame = dicom.GetInstanceDecodedFrame(0) + print('Size of the frame: %dx%d' % (frame.GetImageWidth(), frame.GetImageHeight())) + +orthanc.RegisterOnChangeCallback(OnChange) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/dicom-find-move-scp.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/dicom-find-move-scp.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,24 @@ +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)) + +orthanc.RegisterFindCallback(OnFind) +orthanc.RegisterMoveCallback(OnMove) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/excel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/excel.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,29 @@ +import StringIO +import json +import orthanc +import xlwt + +def CreateExcelReport(output, uri, **request): + if request['method'] != 'GET' : + output.SendMethodNotAllowed('GET') + else: + # Create an Excel writer + excel = xlwt.Workbook() + sheet = excel.add_sheet('Studies') + + # Loop over the studies stored in Orthanc + row = 0 + studies = orthanc.RestApiGet('/studies?expand') + for study in json.loads(studies): + sheet.write(row, 0, study['PatientMainDicomTags'].get('PatientID')) + sheet.write(row, 1, study['PatientMainDicomTags'].get('PatientName')) + sheet.write(row, 2, study['MainDicomTags'].get('StudyDescription')) + row += 1 + + # Serialize the Excel workbook to a string, and return it to the caller + # https://stackoverflow.com/a/15649139/881731 + b = StringIO.StringIO() + excel.save(b) + output.AnswerBuffer(b.getvalue(), 'application/vnd.ms-excel') + +orthanc.RegisterRestCallback('/report.xls', CreateExcelReport) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/filtering-metadata.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/filtering-metadata.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,72 @@ +import json +import orthanc +import re + +# Get the path in the REST API to the given resource that was returned +# by a call to "/tools/find" +def GetPath(resource): + if resource['Type'] == 'Patient': + return '/patients/%s' % resource['ID'] + elif resource['Type'] == 'Study': + return '/studies/%s' % resource['ID'] + elif resource['Type'] == 'Series': + return '/series/%s' % resource['ID'] + elif resource['Type'] == 'Instance': + return '/instances/%s' % resource['ID'] + else: + raise Exception('Unknown resource level') + +def FindWithMetadata(output, uri, **request): + # The "/tools/find" route expects a POST method + if request['method'] != 'POST': + output.SendMethodNotAllowed('POST') + else: + # Parse the query provided by the user, and backup the "Expand" field + query = json.loads(request['body']) + + if 'Expand' in query: + originalExpand = query['Expand'] + else: + originalExpand = False + + # Call the core "/tools/find" route + query['Expand'] = True + answers = orthanc.RestApiPost('/tools/find', json.dumps(query)) + + # Loop over the matching resources + filteredAnswers = [] + for answer in json.loads(answers): + try: + # Read the metadata that is associated with the resource + metadata = json.loads(orthanc.RestApiGet('%s/metadata?expand' % GetPath(answer))) + + # Check whether the metadata matches the regular expressions + # that were provided in the "Metadata" field of the user request + isMetadataMatch = True + if 'Metadata' in query: + for (name, pattern) in query['Metadata'].items(): + if name in metadata: + value = metadata[name] + else: + value = '' + + if re.match(pattern, value) == None: + isMetadataMatch = False + break + + # If all the metadata matches the provided regular + # expressions, add the resource to the filtered answers + if isMetadataMatch: + if originalExpand: + answer['Metadata'] = metadata + filteredAnswers.append(answer) + else: + filteredAnswers.append(answer['ID']) + except: + # The resource was deleted since the call to "/tools/find" + pass + + # Return the filtered answers in the JSON format + output.AnswerBuffer(json.dumps(filteredAnswers, indent = 3), 'application/json') + +orthanc.RegisterRestCallback('/tools/find', FindWithMetadata) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/paging.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/paging.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,38 @@ +import json +import orthanc + +def GetStudyDate(study): + if 'StudyDate' in study['MainDicomTags']: + return study['MainDicomTags']['StudyDate'] + else: + return '' + +def SortStudiesByDate(output, uri, **request): + if request['method'] == 'GET': + # Retrieve all the studies + studies = json.loads(orthanc.RestApiGet('/studies?expand')) + + # Sort the studies according to the "StudyDate" DICOM tag + studies = sorted(studies, key = GetStudyDate) + + # Read the limit/offset arguments provided by the user + offset = 0 + if 'offset' in request['get']: + offset = int(request['get']['offset']) + + limit = 0 + if 'limit' in request['get']: + limit = int(request['get']['limit']) + + # Truncate the list of studies + if limit == 0: + studies = studies[offset : ] + else: + studies = studies[offset : offset + limit] + + # Return the truncated list of studies + output.AnswerBuffer(json.dumps(studies), 'application/json') + else: + output.SendMethodNotAllowed('GET') + +orthanc.RegisterRestCallback('/sort-studies', SortStudiesByDate) diff -r a589668768d7 -r ba2403ebd4b7 Sphinx/source/plugins/python/periodic-execution.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sphinx/source/plugins/python/periodic-execution.py Fri Jun 11 10:24:08 2021 +0200 @@ -0,0 +1,24 @@ +import orthanc +import threading + +TIMER = None + +def Hello(): + global TIMER + TIMER = None + orthanc.LogWarning("In Hello()") + # Do stuff... + TIMER = threading.Timer(1, Hello) # Re-schedule after 1 second + TIMER.start() + +def OnChange(changeType, level, resource): + if changeType == orthanc.ChangeType.ORTHANC_STARTED: + orthanc.LogWarning("Starting the scheduler") + Hello() + + elif changeType == orthanc.ChangeType.ORTHANC_STOPPED: + if TIMER != None: + orthanc.LogWarning("Stopping the scheduler") + TIMER.cancel() + +orthanc.RegisterOnChangeCallback(OnChange)