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