Mercurial > hg > orthanc-book
comparison Sphinx/source/plugins/python.rst @ 703:a589668768d7
moving python samples in separate files (2)
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 11 Jun 2021 10:07:12 +0200 |
parents | 6e02cd89eb6a |
children | ba2403ebd4b7 |
comparison
equal
deleted
inserted
replaced
702:6e02cd89eb6a | 703:a589668768d7 |
---|---|
213 ------- | 213 ------- |
214 | 214 |
215 Extending the REST API | 215 Extending the REST API |
216 ...................... | 216 ...................... |
217 | 217 |
218 .. highlight:: python | |
219 | |
220 Here is a basic Python script that registers two new routes in the | 218 Here is a basic Python script that registers two new routes in the |
221 REST API:: | 219 REST API: |
222 | 220 |
223 import orthanc | 221 .. literalinclude:: python/extending-rest-api.py |
224 import pprint | 222 :language: python |
225 | |
226 def OnRest(output, uri, **request): | |
227 pprint.pprint(request) | |
228 print('Accessing uri: %s' % uri) | |
229 output.AnswerBuffer('ok\n', 'text/plain') | |
230 | |
231 orthanc.RegisterRestCallback('/(to)(t)o', OnRest) | |
232 orthanc.RegisterRestCallback('/tata', OnRest) | |
233 | 223 |
234 .. highlight:: json | 224 .. highlight:: json |
235 | 225 |
236 Here is the associated minimal configuration file for Orthanc | 226 Here is the associated minimal configuration file for Orthanc |
237 (provided the Python script is saved as ``rest.py``):: | 227 (provided the Python script is saved as ``rest.py``):: |
253 .. _python-changes: | 243 .. _python-changes: |
254 | 244 |
255 Listening to changes | 245 Listening to changes |
256 .................... | 246 .................... |
257 | 247 |
258 .. highlight:: python | 248 This sample uploads a DICOM file as soon as Orthanc is started: |
259 | 249 |
260 This sample uploads a DICOM file as soon as Orthanc is started:: | 250 .. literalinclude:: python/listening-changes.py |
261 | 251 :language: python |
262 import orthanc | |
263 | |
264 def OnChange(changeType, level, resource): | |
265 if changeType == orthanc.ChangeType.ORTHANC_STARTED: | |
266 print('Started') | |
267 | |
268 with open('/tmp/sample.dcm', 'rb') as f: | |
269 orthanc.RestApiPost('/instances', f.read()) | |
270 | |
271 elif changeType == orthanc.ChangeType.ORTHANC_STOPPED: | |
272 print('Stopped') | |
273 | |
274 elif changeType == orthanc.ChangeType.NEW_INSTANCE: | |
275 print('A new instance was uploaded: %s' % resource) | |
276 | |
277 orthanc.RegisterOnChangeCallback(OnChange) | |
278 | |
279 | 252 |
280 | 253 |
281 .. warning:: | 254 .. warning:: |
282 In releases <= 3.0 of the Python plugin, deadlocks might emerge if | 255 In releases <= 3.0 of the Python plugin, deadlocks might emerge if |
283 you call other core primitives of Orthanc (such as the REST API) in | 256 you call other core primitives of Orthanc (such as the REST API) in |
287 As a **temporary workaround** against such deadlocks in releases <= | 260 As a **temporary workaround** against such deadlocks in releases <= |
288 3.0, if you have to call other primitives of Orthanc, you should make | 261 3.0, if you have to call other primitives of Orthanc, you should make |
289 these calls in a separate thread, passing the pending events to be | 262 these calls in a separate thread, passing the pending events to be |
290 processed through a message queue. Here is the template of a possible | 263 processed through a message queue. Here is the template of a possible |
291 solution to postpone such deadlocks as much as possible by relying on | 264 solution to postpone such deadlocks as much as possible by relying on |
292 the multithreading primitives of Python:: | 265 the multithreading primitives of Python: |
293 | 266 |
294 import orthanc | 267 .. literalinclude:: python/changes-deadlock-3.0.py |
295 import threading | 268 :language: python |
296 | |
297 def OnChange(changeType, level, resource): | |
298 # One can safely invoke the "orthanc" module in this function | |
299 orthanc.LogWarning("Hello world") | |
300 | |
301 def _OnChange(changeType, level, resource): | |
302 # Invoke the actual "OnChange()" function in a separate thread | |
303 t = threading.Timer(0, function = OnChange, args = (changeType, level, resource)) | |
304 t.start() | |
305 | |
306 orthanc.RegisterOnChangeCallback(_OnChange) | |
307 | |
308 | 269 |
309 Beware that **this workaround is imperfect** and deadlocks have been | 270 Beware that **this workaround is imperfect** and deadlocks have been |
310 observed even if using it! Make sure to upgrade your plugin to solve | 271 observed even if using it! Make sure to upgrade your plugin to solve |
311 this issue for good. Note that this temporary workaround is not needed | 272 this issue for good. Note that this temporary workaround is not needed |
312 in releases >= 3.1 of the plugin. | 273 in releases >= 3.1 of the plugin. |
314 | 275 |
315 | 276 |
316 Accessing the content of a new instance | 277 Accessing the content of a new instance |
317 ....................................... | 278 ....................................... |
318 | 279 |
319 .. highlight:: python | 280 .. literalinclude:: python/accessing-new-instance.py |
320 | 281 :language: python |
321 :: | |
322 | |
323 import orthanc | |
324 import json | |
325 import pprint | |
326 | |
327 def OnStoredInstance(dicom, instanceId): | |
328 print('Received instance %s of size %d (transfer syntax %s, SOP class UID %s)' % ( | |
329 instanceId, dicom.GetInstanceSize(), | |
330 dicom.GetInstanceMetadata('TransferSyntax'), | |
331 dicom.GetInstanceMetadata('SopClassUid'))) | |
332 | |
333 # Print the origin information | |
334 if dicom.GetInstanceOrigin() == orthanc.InstanceOrigin.DICOM_PROTOCOL: | |
335 print('This instance was received through the DICOM protocol') | |
336 elif dicom.GetInstanceOrigin() == orthanc.InstanceOrigin.REST_API: | |
337 print('This instance was received through the REST API') | |
338 | |
339 # Print the DICOM tags | |
340 pprint.pprint(json.loads(dicom.GetInstanceSimplifiedJson())) | |
341 | |
342 orthanc.RegisterOnStoredInstanceCallback(OnStoredInstance) | |
343 | |
344 | 282 |
345 .. warning:: | 283 .. warning:: |
346 Your callback function will be called synchronously with | 284 Your callback function will be called synchronously with |
347 the core of Orthanc. This implies that deadlocks might emerge if | 285 the core of Orthanc. This implies that deadlocks might emerge if |
348 you call other core primitives of Orthanc in your callback (such | 286 you call other core primitives of Orthanc in your callback (such |
355 | 293 |
356 | 294 |
357 Calling pydicom | 295 Calling pydicom |
358 ............... | 296 ............... |
359 | 297 |
360 .. highlight:: python | |
361 | |
362 Here is a sample Python plugin that registers a REST callback to dump | 298 Here is a sample Python plugin that registers a REST callback to dump |
363 the content of the dataset of one given DICOM instance stored in | 299 the content of the dataset of one given DICOM instance stored in |
364 Orthanc, using `pydicom <https://pydicom.github.io/>`__:: | 300 Orthanc, using `pydicom <https://pydicom.github.io/>`__: |
365 | 301 |
366 import io | 302 .. literalinclude:: python/pydicom.py |
367 import orthanc | 303 :language: python |
368 import pydicom | |
369 | |
370 def DecodeInstance(output, uri, **request): | |
371 if request['method'] == 'GET': | |
372 # Retrieve the instance ID from the regular expression (*) | |
373 instanceId = request['groups'][0] | |
374 # Get the content of the DICOM file | |
375 f = orthanc.GetDicomForInstance(instanceId) | |
376 # Parse it using pydicom | |
377 dicom = pydicom.dcmread(io.BytesIO(f)) | |
378 # Return a string representation the dataset to the caller | |
379 output.AnswerBuffer(str(dicom), 'text/plain') | |
380 else: | |
381 output.SendMethodNotAllowed('GET') | |
382 | |
383 orthanc.RegisterRestCallback('/pydicom/(.*)', DecodeInstance) # (*) | |
384 | 304 |
385 .. highlight:: bash | 305 .. highlight:: bash |
386 | 306 |
387 This can be called as follows:: | 307 This callback can be called as follows:: |
388 | 308 |
389 $ curl http://localhost:8042/pydicom/19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5 | 309 $ curl http://localhost:8042/pydicom/19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5 |
390 | 310 |
391 | 311 |
392 Auto-routing studies | 312 Auto-routing studies |
393 .................... | 313 .................... |
394 | 314 |
395 .. highlight:: python | |
396 | |
397 Here is a sample Python plugin that routes any :ref:`stable study | 315 Here is a sample Python plugin that routes any :ref:`stable study |
398 <stable-resources>` to a modality named ``samples`` (as declared in the | 316 <stable-resources>` to a modality named ``samples`` (as declared in the |
399 ``DicomModalities`` configuration option):: | 317 ``DicomModalities`` configuration option): |
400 | 318 |
401 import orthanc | 319 .. literalinclude:: python/autorouting-1.py |
402 | 320 :language: python |
403 def OnChange(changeType, level, resourceId): | |
404 if changeType == orthanc.ChangeType.STABLE_STUDY: | |
405 print('Stable study: %s' % resourceId) | |
406 orthanc.RestApiPost('/modalities/sample/store', resourceId) | |
407 | |
408 orthanc.RegisterOnChangeCallback(OnChange) | |
409 | |
410 | 321 |
411 Note that, if you want to use an orthanc plugin to transfer the study, | 322 Note that, if you want to use an orthanc plugin to transfer the study, |
412 you should use the ``RestApiPostAfterPlugins()`` method:: | 323 you should use the ``RestApiPostAfterPlugins()`` method: |
413 | 324 |
414 import orthanc | 325 .. literalinclude:: python/autorouting-2.py |
415 | 326 :language: python |
416 def OnChange(changeType, level, resourceId): | 327 |
417 if changeType == orthanc.ChangeType.STABLE_STUDY: | |
418 print('Stable study: %s' % resourceId) | |
419 orthanc.RestApiPostAfterPlugins('/dicom-web/servers/sample/store', resourceId) | |
420 | |
421 orthanc.RegisterOnChangeCallback(OnChange) | |
422 | |
423 | 328 |
424 Rendering a thumbnail using PIL/Pillow | 329 Rendering a thumbnail using PIL/Pillow |
425 ...................................... | 330 ...................................... |
426 | 331 |
427 .. highlight:: python | 332 .. literalinclude:: python/pil.py |
428 | 333 :language: python |
429 :: | |
430 | |
431 from PIL import Image | |
432 import io | |
433 import orthanc | |
434 | |
435 def DecodeInstance(output, uri, **request): | |
436 if request['method'] == 'GET': | |
437 # Retrieve the instance ID from the regular expression (*) | |
438 instanceId = request['groups'][0] | |
439 | |
440 # Render the instance, then open it in Python using PIL/Pillow | |
441 png = orthanc.RestApiGet('/instances/%s/rendered' % instanceId) | |
442 image = Image.open(io.BytesIO(png)) | |
443 | |
444 # Downsize the image as a 64x64 thumbnail | |
445 image.thumbnail((64, 64), Image.ANTIALIAS) | |
446 | |
447 # Save the thumbnail as JPEG, then send the buffer to the caller | |
448 jpeg = io.BytesIO() | |
449 image.save(jpeg, format = "JPEG", quality = 80) | |
450 jpeg.seek(0) | |
451 output.AnswerBuffer(jpeg.read(), 'text/plain') | |
452 | |
453 else: | |
454 output.SendMethodNotAllowed('GET') | |
455 | |
456 orthanc.RegisterRestCallback('/pydicom/(.*)', DecodeInstance) # (*) | |
457 | 334 |
458 | 335 |
459 .. _python-introspection: | 336 .. _python-introspection: |
460 | 337 |
461 Inspecting the available API | 338 Inspecting the available API |
462 ............................ | 339 ............................ |
463 | 340 |
464 .. highlight:: python | |
465 | |
466 Thanks to Python's introspection primitives, it is possible to inspect | 341 Thanks to Python's introspection primitives, it is possible to inspect |
467 the API of the ``orthanc`` module in order to dump all the available | 342 the API of the ``orthanc`` module in order to dump all the available |
468 features:: | 343 features: |
469 | 344 |
470 import inspect | 345 .. literalinclude:: python/inspect-api.py |
471 import numbers | 346 :language: python |
472 import orthanc | 347 |
473 | 348 |
474 # Loop over the members of the "orthanc" module | |
475 for (name, obj) in inspect.getmembers(orthanc): | |
476 if inspect.isroutine(obj): | |
477 print('Function %s():\n Documentation: %s\n' % (name, inspect.getdoc(obj))) | |
478 | |
479 elif inspect.isclass(obj): | |
480 print('Class %s:\n Documentation: %s' % (name, inspect.getdoc(obj))) | |
481 | |
482 # Loop over the members of the class | |
483 for (subname, subobj) in inspect.getmembers(obj): | |
484 if isinstance(subobj, numbers.Number): | |
485 print(' - Enumeration value %s: %s' % (subname, subobj)) | |
486 elif (not subname.startswith('_') and | |
487 inspect.ismethoddescriptor(subobj)): | |
488 print(' - Method %s(): %s' % (subname, inspect.getdoc(subobj))) | |
489 print('') | |
490 | |
491 | |
492 .. _python-scheduler: | 349 .. _python-scheduler: |
493 | 350 |
494 Scheduling a task for periodic execution | 351 Scheduling a task for periodic execution |
495 ........................................ | 352 ........................................ |
496 | 353 |