changeset 83:3f2170efa8d2

patches for python3
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 23 Jun 2016 18:04:59 +0200
parents 91e2ed032f96
children dc90b87471a8
files Plugins/DicomWeb/DicomWeb.py Plugins/DicomWeb/Run.py Tests/Run.py Tests/Tests.py Tests/Toolbox.py
diffstat 5 files changed, 92 insertions(+), 63 deletions(-) [+]
line wrap: on
line diff
--- a/Plugins/DicomWeb/DicomWeb.py	Tue Jun 21 16:33:42 2016 +0200
+++ b/Plugins/DicomWeb/DicomWeb.py	Thu Jun 23 18:04:59 2016 +0200
@@ -21,7 +21,7 @@
 import os
 import sys
 import email
-import urllib2
+import uuid
 
 from email.mime.multipart import MIMEMultipart
 from email.mime.application import MIMEApplication
@@ -30,48 +30,38 @@
 from Toolbox import *
 
 
-def _AttachPart(related, path, contentType):
+def _AttachPart(body, path, contentType, boundary):
     with open(path, 'rb') as f:
-        part = MIMEApplication(f.read(), contentType, email.encoders.encode_noop)
-        related.attach(part)
+        body += bytearray('--%s\r\n' % boundary, 'ascii')
+        body += bytearray('Content-Type: %s\r\n\r\n' % contentType, 'ascii')
+        body += f.read()
+        body += bytearray('\r\n', 'ascii')
 
 
 def SendStow(orthanc, uri, dicom):
-    related = MIMEMultipart('related')
-    related.set_boundary('boundary_0123456789_boundary')
+    # We do not use Python's "email" package, as it uses LF (\n) for line
+    # endings instead of CRLF (\r\n) for binary messages, as required by
+    # RFC 1341
+    # http://stackoverflow.com/questions/3086860/how-do-i-generate-a-multipart-mime-message-with-correct-crlf-in-python
+    # https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
+
+    # Create a multipart message whose body contains all the input DICOM files
+    boundary = str(uuid.uuid4())  # The boundary is a random UUID
+    body = bytearray()
 
     if isinstance(dicom, list):
         for i in range(dicom):
-            _AttachPart(related, dicom[i], 'dicom')
+            _AttachPart(body, dicom[i], 'application/dicom', boundary)
     else:
-        _AttachPart(related, dicom, 'dicom')
+        _AttachPart(body, dicom, 'application/dicom', boundary)
 
-    headers = dict(related.items())
-    body = related.as_string()
+    # Closing boundary
+    body += bytearray('--%s--' % boundary, 'ascii')
 
-    # Discard the header
-    body = body.split('\n\n', 1)[1]
-
-    headers['Content-Type'] = 'multipart/related; type=application/dicom; boundary=%s' % related.get_boundary()
-    headers['Accept'] = 'application/json'
+    # Do the HTTP POST request to the STOW-RS server
+    headers = {
+        'Content-Type' : 'multipart/related; type=application/dicom; boundary=%s' % boundary,
+        'Accept' : 'application/json',
+    }
 
     return DoPost(orthanc, uri, body, headers = headers)
-
-
-def GetMultipart(uri, headers = {}):
-    tmp = urllib2.urlopen(uri)
-    info = str(tmp.info())
-    answer = tmp.read()
-
-    s = info + "\n" + answer
-
-    msg = email.message_from_string(s)
-
-    result = []
-
-    for i, part in enumerate(msg.walk(), 1):
-        payload = part.get_payload(decode = True)
-        if payload != None:
-            result.append(payload)
-
-    return result
--- a/Plugins/DicomWeb/Run.py	Tue Jun 21 16:33:42 2016 +0200
+++ b/Plugins/DicomWeb/Run.py	Thu Jun 23 18:04:59 2016 +0200
@@ -23,6 +23,7 @@
 import sys
 import argparse
 import unittest
+import re
 from DicomWeb import *
 
 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Tests'))
@@ -90,6 +91,11 @@
 
 class Orthanc(unittest.TestCase):
     def setUp(self):
+        if (sys.version_info >= (3, 0)):
+            # Remove annoying warnings about unclosed socket in Python 3
+            import warnings
+            warnings.simplefilter("ignore", ResourceWarning)
+
         DropOrthanc(ORTHANC)
 
     def test_wado_dicom(self):
@@ -156,9 +162,15 @@
         self.assertTrue(a['00081199']['Value'][0]['00081190']['Value'][0].
                         endswith('series/1.2.840.113704.1.111.5692.1127828999.2/instances/1.2.840.113704.7.1.1.6632.1127829031.2'))
 
-        b = GetMultipart(a['00081190']['Value'][0])
-        self.assertEqual(1, len(b))
-        self.assertEqual(368852, len(b[0]))
+        # Remove the "http://localhost:8042" prefix
+        url = a['00081190']['Value'][0]
+        url = re.sub(r'(http|https)://[^/]+(/.*)', r'\2', url)
+
+        # Get the content-length of all the multiparts of this WADO-RS request
+        b = DoGet(ORTHANC, url).decode('utf-8', 'ignore')
+        parts = re.findall(r'^Content-Length:\s*(\d+)\s*', b, re.IGNORECASE | re.MULTILINE)
+        self.assertEqual(1, len(parts))
+        self.assertEqual(os.path.getsize(GetDatabasePath('Phenix/IM-0001-0001.dcm')), int(parts[0]))
 
 
 
--- a/Tests/Run.py	Tue Jun 21 16:33:42 2016 +0200
+++ b/Tests/Run.py	Thu Jun 23 18:04:59 2016 +0200
@@ -146,8 +146,10 @@
 try:
     print('\nStarting the tests...')
     SetOrthancParameters(LOCAL, REMOTE)
+
     unittest.main(argv = [ sys.argv[0] ] + args.options)
 
+
 finally:
     print('\nDone')
 
--- a/Tests/Tests.py	Tue Jun 21 16:33:42 2016 +0200
+++ b/Tests/Tests.py	Thu Jun 23 18:04:59 2016 +0200
@@ -129,6 +129,11 @@
 
 class Orthanc(unittest.TestCase):
     def setUp(self):
+        if (sys.version_info >= (3, 0)):
+            # Remove annoying warnings about unclosed socket in Python 3
+            import warnings
+            warnings.simplefilter("ignore", ResourceWarning)
+
         DropOrthanc(_LOCAL)
         DropOrthanc(_REMOTE)
         UninstallLuaCallbacks()
@@ -531,7 +536,7 @@
 
         self.assertNotEqual('hello', DoGet(_REMOTE, '/instances/%s/content/0010-0010' % i).strip())
         #self.assertNotEqual('world', DoGet(_REMOTE, '/instances/%s/content/0010-0020' % i).strip())
-        self.assertEqual('LOGIQBOOK', DoGet(_REMOTE, '/instances/%s/content/0008-1010' % i).strip())        
+        self.assertEqual('LOGIQBOOK', DoGet(_REMOTE, '/instances/%s/content/0008-1010' % i).strip())
         DoGet(_REMOTE, '/instances/%s/content/6003-1010' % i)  # Some private tag
 
         self.assertEqual('hello', DoGet(_REMOTE, '/instances/%s/content/0010-0010' % j).strip())
--- a/Tests/Toolbox.py	Tue Jun 21 16:33:42 2016 +0200
+++ b/Tests/Toolbox.py	Thu Jun 23 18:04:59 2016 +0200
@@ -26,18 +26,40 @@
 import signal
 import subprocess
 import threading
+import sys
 import time
 import zipfile
 
 from PIL import Image
-from urllib import urlencode
+
+if (sys.version_info >= (3, 0)):
+    from urllib.parse import urlencode
+    from io import StringIO
+    from io import BytesIO
+
+else:
+    from urllib import urlencode
+
+    # http://stackoverflow.com/a/1313868/881731
+    try:
+        from cStringIO import StringIO
+    except:
+        from StringIO import StringIO
 
 
-# http://stackoverflow.com/a/1313868/881731
-try:
-    from cStringIO import StringIO
-except:
-    from StringIO import StringIO
+def _DecodeJson(s):
+    t = s
+
+    if (sys.version_info >= (3, 0)):
+        try:
+            t = s.decode()
+        except:
+            pass
+
+    try:
+        return json.loads(t)
+    except:
+        return t
 
 
 def DefineOrthanc(server = 'localhost',
@@ -90,17 +112,14 @@
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 def _DoPutOrPost(orthanc, uri, method, data, contentType, headers):
     http = httplib2.Http()
     http.follow_redirects = False
     _SetupCredentials(orthanc, http)
 
-    if isinstance(data, str):
+    if isinstance(data, (str, bytearray, bytes)):
         body = data
         if len(contentType) != 0:
             headers['content-type'] = contentType
@@ -116,10 +135,7 @@
     if not (resp.status in [ 200, 302 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 def DoDelete(orthanc, uri):
     http = httplib2.Http()
@@ -130,10 +146,7 @@
     if not (resp.status in [ 200 ]):
         raise Exception(resp.status)
     else:
-        try:
-            return json.loads(content)
-        except:
-            return content
+        return _DecodeJson(content)
 
 def DoPut(orthanc, uri, data = {}, contentType = ''):
     return _DoPutOrPost(orthanc, uri, 'PUT', data, contentType, {})
@@ -145,9 +158,9 @@
     return os.path.join(os.path.dirname(__file__), '..', 'Database', filename)
 
 def UploadInstance(orthanc, filename):
-    f = open(GetDatabasePath(filename), 'rb')
-    d = f.read()
-    f.close()
+    with open(GetDatabasePath(filename), 'rb') as f:
+        d = f.read()
+
     return DoPost(orthanc, '/instances', d, 'application/dicom')
 
 def UploadFolder(orthanc, path):
@@ -174,12 +187,19 @@
 def GetImage(orthanc, uri, headers = {}):
     # http://www.pythonware.com/library/pil/handbook/introduction.htm
     data = DoGet(orthanc, uri, headers = headers)
-    return Image.open(StringIO(data))
+    if (sys.version_info >= (3, 0)):
+        return Image.open(BytesIO(data))
+    else:
+        return Image.open(StringIO(data))
 
 def GetArchive(orthanc, uri):
     # http://stackoverflow.com/a/1313868/881731
     s = DoGet(orthanc, uri)
-    return zipfile.ZipFile(StringIO(s), "r")
+
+    if (sys.version_info >= (3, 0)):
+        return zipfile.ZipFile(BytesIO(s), "r")
+    else:
+        return zipfile.ZipFile(StringIO(s), "r")
 
 def IsDefinedInLua(orthanc, name):
     s = DoPost(orthanc, '/tools/execute-script', 'print(type(%s))' % name, 'application/lua')
@@ -224,7 +244,7 @@
                 os._exit(-1)
             stop_event.wait(0.1)
 
-        print 'Stopping the external command'
+        print('Stopping the external command')
         external.terminate()
         external.communicate()  # Wait for the command to stop