comparison Sphinx/source/plugins/python.rst @ 704:ba2403ebd4b7

moving python samples in separate files (3)
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 11 Jun 2021 10:24:08 +0200
parents a589668768d7
children c62539d00251
comparison
equal deleted inserted replaced
703:a589668768d7 704:ba2403ebd4b7
349 .. _python-scheduler: 349 .. _python-scheduler:
350 350
351 Scheduling a task for periodic execution 351 Scheduling a task for periodic execution
352 ........................................ 352 ........................................
353 353
354 .. highlight:: python
355
356 The following Python script will periodically (every second) run the 354 The following Python script will periodically (every second) run the
357 function ``Hello()`` thanks to the ``threading`` module:: 355 function ``Hello()`` thanks to the ``threading`` module:
358 356
359 import orthanc 357 .. literalinclude:: python/periodic-execution.py
360 import threading 358 :language: python
361
362 TIMER = None
363
364 def Hello():
365 global TIMER
366 TIMER = None
367 orthanc.LogWarning("In Hello()")
368 # Do stuff...
369 TIMER = threading.Timer(1, Hello) # Re-schedule after 1 second
370 TIMER.start()
371
372 def OnChange(changeType, level, resource):
373 if changeType == orthanc.ChangeType.ORTHANC_STARTED:
374 orthanc.LogWarning("Starting the scheduler")
375 Hello()
376
377 elif changeType == orthanc.ChangeType.ORTHANC_STOPPED:
378 if TIMER != None:
379 orthanc.LogWarning("Stopping the scheduler")
380 TIMER.cancel()
381
382 orthanc.RegisterOnChangeCallback(OnChange)
383 359
384 360
385 .. _python-metadata: 361 .. _python-metadata:
386 362
387 Filtering and returning metadata 363 Filtering and returning metadata
397 This feature is not built in the core of Orthanc, as metadata is not 373 This feature is not built in the core of Orthanc, as metadata is not
398 indexed in the Orthanc database, contrarily to the main DICOM 374 indexed in the Orthanc database, contrarily to the main DICOM
399 tags. Filtering metadata requires a linear search over all the 375 tags. Filtering metadata requires a linear search over all the
400 matching resources, which induces a cost in the performance. 376 matching resources, which induces a cost in the performance.
401 377
402 .. highlight:: python
403
404 Nevertheless, here is a full sample Python script that overwrites the 378 Nevertheless, here is a full sample Python script that overwrites the
405 ``/tools/find`` route in order to give access to metadata:: 379 ``/tools/find`` route in order to give access to metadata:
406 380
407 import json 381 .. literalinclude:: python/filtering-metadata.py
408 import orthanc 382 :language: python
409 import re
410
411 # Get the path in the REST API to the given resource that was returned
412 # by a call to "/tools/find"
413 def GetPath(resource):
414 if resource['Type'] == 'Patient':
415 return '/patients/%s' % resource['ID']
416 elif resource['Type'] == 'Study':
417 return '/studies/%s' % resource['ID']
418 elif resource['Type'] == 'Series':
419 return '/series/%s' % resource['ID']
420 elif resource['Type'] == 'Instance':
421 return '/instances/%s' % resource['ID']
422 else:
423 raise Exception('Unknown resource level')
424
425 def FindWithMetadata(output, uri, **request):
426 # The "/tools/find" route expects a POST method
427 if request['method'] != 'POST':
428 output.SendMethodNotAllowed('POST')
429 else:
430 # Parse the query provided by the user, and backup the "Expand" field
431 query = json.loads(request['body'])
432
433 if 'Expand' in query:
434 originalExpand = query['Expand']
435 else:
436 originalExpand = False
437
438 # Call the core "/tools/find" route
439 query['Expand'] = True
440 answers = orthanc.RestApiPost('/tools/find', json.dumps(query))
441
442 # Loop over the matching resources
443 filteredAnswers = []
444 for answer in json.loads(answers):
445 try:
446 # Read the metadata that is associated with the resource
447 metadata = json.loads(orthanc.RestApiGet('%s/metadata?expand' % GetPath(answer)))
448
449 # Check whether the metadata matches the regular expressions
450 # that were provided in the "Metadata" field of the user request
451 isMetadataMatch = True
452 if 'Metadata' in query:
453 for (name, pattern) in query['Metadata'].items():
454 if name in metadata:
455 value = metadata[name]
456 else:
457 value = ''
458
459 if re.match(pattern, value) == None:
460 isMetadataMatch = False
461 break
462
463 # If all the metadata matches the provided regular
464 # expressions, add the resource to the filtered answers
465 if isMetadataMatch:
466 if originalExpand:
467 answer['Metadata'] = metadata
468 filteredAnswers.append(answer)
469 else:
470 filteredAnswers.append(answer['ID'])
471 except:
472 # The resource was deleted since the call to "/tools/find"
473 pass
474
475 # Return the filtered answers in the JSON format
476 output.AnswerBuffer(json.dumps(filteredAnswers, indent = 3), 'application/json')
477
478 orthanc.RegisterRestCallback('/tools/find', FindWithMetadata)
479 383
480 384
481 **Warning:** In the sample above, the filtering of the metadata is 385 **Warning:** In the sample above, the filtering of the metadata is
482 done using Python's `library for regular expressions 386 done using Python's `library for regular expressions
483 <https://docs.python.org/3/library/re.html>`__. It is evidently 387 <https://docs.python.org/3/library/re.html>`__. It is evidently
496 .. _python-paging: 400 .. _python-paging:
497 401
498 Implementing basic paging 402 Implementing basic paging
499 ......................... 403 .........................
500 404
501 .. highlight:: python
502
503 As explained in the FAQ, the :ref:`Orthanc Explorer interface is 405 As explained in the FAQ, the :ref:`Orthanc Explorer interface is
504 low-level <improving-interface>`, and is not adapted for 406 low-level <improving-interface>`, and is not adapted for
505 end-users. One common need is to implement paging of studies, which 407 end-users. One common need is to implement paging of studies, which
506 calls for server-side sorting of studies. This can be done using the 408 calls for server-side sorting of studies. This can be done using the
507 following sample Python plugin that registers a new route 409 following sample Python plugin that registers a new route
508 ``/sort-studies`` in the REST API of Orthanc:: 410 ``/sort-studies`` in the REST API of Orthanc:
509 411
510 import json 412 .. literalinclude:: python/paging.py
511 import orthanc 413 :language: python
512
513 def GetStudyDate(study):
514 if 'StudyDate' in study['MainDicomTags']:
515 return study['MainDicomTags']['StudyDate']
516 else:
517 return ''
518
519 def SortStudiesByDate(output, uri, **request):
520 if request['method'] == 'GET':
521 # Retrieve all the studies
522 studies = json.loads(orthanc.RestApiGet('/studies?expand'))
523
524 # Sort the studies according to the "StudyDate" DICOM tag
525 studies = sorted(studies, key = GetStudyDate)
526
527 # Read the limit/offset arguments provided by the user
528 offset = 0
529 if 'offset' in request['get']:
530 offset = int(request['get']['offset'])
531
532 limit = 0
533 if 'limit' in request['get']:
534 limit = int(request['get']['limit'])
535
536 # Truncate the list of studies
537 if limit == 0:
538 studies = studies[offset : ]
539 else:
540 studies = studies[offset : offset + limit]
541
542 # Return the truncated list of studies
543 output.AnswerBuffer(json.dumps(studies), 'application/json')
544 else:
545 output.SendMethodNotAllowed('GET')
546
547 orthanc.RegisterRestCallback('/sort-studies', SortStudiesByDate)
548
549 414
550 .. highlight:: bash 415 .. highlight:: bash
551 416
552 Here is a sample call to this new REST route, that could be issued by 417 Here is a sample call to this new REST route, that could be issued by
553 any JavaScript framework (the ``json_pp`` command-line pretty-prints a 418 any JavaScript framework (the ``json_pp`` command-line pretty-prints a
571 .. _python_excel: 436 .. _python_excel:
572 437
573 Creating a Microsoft Excel report 438 Creating a Microsoft Excel report
574 ................................. 439 .................................
575 440
576 .. highlight:: python
577
578 As Orthanc plugins have access to any installed Python module, it is 441 As Orthanc plugins have access to any installed Python module, it is
579 very easy to implement a server-side plugin that generates a report in 442 very easy to implement a server-side plugin that generates a report in
580 the Microsoft Excel ``.xls`` format. Here is a working example:: 443 the Microsoft Excel ``.xls`` format. Here is a working example:
581 444
582 import StringIO 445 .. literalinclude:: python/excel.py
583 import json 446 :language: python
584 import orthanc
585 import xlwt
586
587 def CreateExcelReport(output, uri, **request):
588 if request['method'] != 'GET' :
589 output.SendMethodNotAllowed('GET')
590 else:
591 # Create an Excel writer
592 excel = xlwt.Workbook()
593 sheet = excel.add_sheet('Studies')
594
595 # Loop over the studies stored in Orthanc
596 row = 0
597 studies = orthanc.RestApiGet('/studies?expand')
598 for study in json.loads(studies):
599 sheet.write(row, 0, study['PatientMainDicomTags'].get('PatientID'))
600 sheet.write(row, 1, study['PatientMainDicomTags'].get('PatientName'))
601 sheet.write(row, 2, study['MainDicomTags'].get('StudyDescription'))
602 row += 1
603
604 # Serialize the Excel workbook to a string, and return it to the caller
605 # https://stackoverflow.com/a/15649139/881731
606 b = StringIO.StringIO()
607 excel.save(b)
608 output.AnswerBuffer(b.getvalue(), 'application/vnd.ms-excel')
609
610 orthanc.RegisterRestCallback('/report.xls', CreateExcelReport)
611 447
612 If opening the ``http://localhost:8042/report.xls`` URI, this Python 448 If opening the ``http://localhost:8042/report.xls`` URI, this Python
613 will generate a workbook with one sheet that contains the list of 449 will generate a workbook with one sheet that contains the list of
614 studies, with the patient ID, the patient name and the study 450 studies, with the patient ID, the patient name and the study
615 description. 451 description.
618 .. _python_authorization: 454 .. _python_authorization:
619 455
620 Forbid or allow access to REST resources (authorization, new in 3.0) 456 Forbid or allow access to REST resources (authorization, new in 3.0)
621 .................................................................... 457 ....................................................................
622 458
623 .. highlight:: python
624
625 The following Python script installs a callback that is triggered 459 The following Python script installs a callback that is triggered
626 whenever the HTTP server of Orthanc is accessed:: 460 whenever the HTTP server of Orthanc is accessed:
627 461
628 import orthanc 462 .. literalinclude:: python/authorization-1.py
629 import pprint 463 :language: python
630 464
631 def Filter(uri, **request):
632 print('User trying to access URI: %s' % uri)
633 pprint.pprint(request)
634 return True # False to forbid access
635
636 orthanc.RegisterIncomingHttpRequestFilter(Filter)
637 465
638 If access is not granted, the ``Filter`` callback must return 466 If access is not granted, the ``Filter`` callback must return
639 ``False``. As a consequence, the HTTP status code would be set to 467 ``False``. As a consequence, the HTTP status code would be set to
640 ``403 Forbidden``. If access is granted, the ``Filter`` must return 468 ``403 Forbidden``. If access is granted, the ``Filter`` must return
641 ``true``. The ``request`` argument contains more information about the 469 ``true``. The ``request`` argument contains more information about the
644 472
645 Note that this is similar to the ``IncomingHttpRequestFilter()`` 473 Note that this is similar to the ``IncomingHttpRequestFilter()``
646 callback that is available in :ref:`Lua scripts <lua-filter-rest>`. 474 callback that is available in :ref:`Lua scripts <lua-filter-rest>`.
647 475
648 Thanks to Python, it is extremely easy to call remote Web services for 476 Thanks to Python, it is extremely easy to call remote Web services for
649 authorization. Here is an example using the ``requests`` library:: 477 authorization. Here is an example using the ``requests`` library:
650 478
651 import json 479 .. literalinclude:: python/authorization-2.py
652 import orthanc 480 :language: python
653 import requests
654
655 def Filter(uri, **request):
656 body = {
657 'uri' : uri,
658 'headers' : request['headers']
659 }
660 r = requests.post('http://localhost:8000/authorize',
661 data = json.dumps(body))
662 return r.json() ['granted'] # Must be a Boolean
663
664 orthanc.RegisterIncomingHttpRequestFilter(Filter)
665 481
666 .. highlight:: javascript 482 .. highlight:: javascript
667 483
668 This filter could be used together with the following Web service 484 This filter could be used together with the following Web service
669 implemented using `Node.js 485 implemented using `Node.js
670 <https://en.wikipedia.org/wiki/Node.js>`__:: 486 <https://en.wikipedia.org/wiki/Node.js>`__:
671 487
672 const http = require('http'); 488 .. literalinclude:: python/authorization-node-service.js
673 489 :language: javascript
674 const requestListener = function(req, res) {
675 let body = '';
676 req.on('data', function(chunk) {
677 body += chunk;
678 });
679 req.on('end', function() {
680 console.log(JSON.parse(body));
681 var answer = {
682 'granted' : false // Forbid access
683 };
684 res.writeHead(200);
685 res.end(JSON.stringify(answer));
686 });
687 }
688
689 http.createServer(requestListener).listen(8000);
690 490
691 491
692 .. _python_create_dicom: 492 .. _python_create_dicom:
693 493
694 Creating DICOM instances (new in 3.2) 494 Creating DICOM instances (new in 3.2)
695 ..................................... 495 .....................................
696 496
697 .. highlight:: python
698
699 The following sample Python script will write on the disk a new DICOM 497 The following sample Python script will write on the disk a new DICOM
700 instance including the traditional Lena sample image, and will decode 498 instance including the traditional Lena sample image, and will decode
701 the single frame of this DICOM instance:: 499 the single frame of this DICOM instance:
702 500
703 import json 501 .. literalinclude:: python/create-dicom.py
704 import orthanc 502 :language: python
705
706 def OnChange(changeType, level, resource):
707 if changeType == orthanc.ChangeType.ORTHANC_STARTED:
708 tags = {
709 'SOPClassUID' : '1.2.840.10008.5.1.4.1.1.1',
710 'PatientID' : 'HELLO',
711 'PatientName' : 'WORLD',
712 }
713
714 with open('Lena.png', 'rb') as f:
715 img = orthanc.UncompressImage(f.read(), orthanc.ImageFormat.PNG)
716
717 s = orthanc.CreateDicom(json.dumps(tags), img, orthanc.CreateDicomFlags.GENERATE_IDENTIFIERS)
718
719 with open('/tmp/sample.dcm', 'wb') as f:
720 f.write(s)
721
722 dicom = orthanc.CreateDicomInstance(s)
723 frame = dicom.GetInstanceDecodedFrame(0)
724 print('Size of the frame: %dx%d' % (frame.GetImageWidth(), frame.GetImageHeight()))
725
726 orthanc.RegisterOnChangeCallback(OnChange)
727 503
728 504
729 .. _python_dicom_scp: 505 .. _python_dicom_scp:
730 506
731 Handling DICOM SCP requests (new in 3.2) 507 Handling DICOM SCP requests (new in 3.2)
732 ........................................ 508 ........................................
733
734 .. highlight:: python
735 509
736 Starting with release 3.2 of the Python plugin, it is possible to 510 Starting with release 3.2 of the Python plugin, it is possible to
737 replace the C-FIND SCP and C-MOVE SCP of Orthanc by a Python 511 replace the C-FIND SCP and C-MOVE SCP of Orthanc by a Python
738 script. This feature can notably be used to create a custom DICOM 512 script. This feature can notably be used to create a custom DICOM
739 proxy. Here is a minimal example:: 513 proxy. Here is a minimal example:
740 514
741 import json 515 .. literalinclude:: python/dicom-find-move-scp.py
742 import orthanc 516 :language: python
743 import pprint 517
744
745 def OnFind(answers, query, issuerAet, calledAet):
746 print('Received incoming C-FIND request from %s:' % issuerAet)
747
748 answer = {}
749 for i in range(query.GetFindQuerySize()):
750 print(' %s (%04x,%04x) = [%s]' % (query.GetFindQueryTagName(i),
751 query.GetFindQueryTagGroup(i),
752 query.GetFindQueryTagElement(i),
753 query.GetFindQueryValue(i)))
754 answer[query.GetFindQueryTagName(i)] = ('HELLO%d-%s' % (i, query.GetFindQueryValue(i)))
755
756 answers.FindAddAnswer(orthanc.CreateDicom(
757 json.dumps(answer), None, orthanc.CreateDicomFlags.NONE))
758
759 def OnMove(**request):
760 orthanc.LogWarning('C-MOVE request to be handled in Python: %s' %
761 json.dumps(request, indent = 4, sort_keys = True))
762
763 orthanc.RegisterFindCallback(OnFind)
764 orthanc.RegisterMoveCallback(OnMove)
765 518
766 .. highlight:: text 519 .. highlight:: text
767 520
768 In this sample, the C-FIND SCP will send one single answer that 521 In this sample, the C-FIND SCP will send one single answer that
769 reproduces the values provided by the SCU:: 522 reproduces the values provided by the SCU::
952 705
953 706
954 Slave processes and the "orthanc" module 707 Slave processes and the "orthanc" module
955 ........................................ 708 ........................................
956 709
957 .. highlight:: python
958
959 Very importantly, pay attention to the fact that **only the "master" 710 Very importantly, pay attention to the fact that **only the "master"
960 Python interpreter has access to the Orthanc SDK**. The "slave" 711 Python interpreter has access to the Orthanc SDK**. The "slave"
961 processes have no access to the ``orthanc`` module. 712 processes have no access to the ``orthanc`` module.
962 713
963 You must write your Python plugin so as that all the calls to 714 You must write your Python plugin so as that all the calls to