├── tests ├── TestConnectVSD.pyc ├── resources │ ├── image.nii │ └── image.xyz └── TestConnectVSD.py ├── contributors.txt ├── examples ├── getVSDFolderIDs.py ├── uploadAndLinkImages.py └── downloadImages.py ├── README.md ├── LICENSE.txt └── source ├── poster.py └── connectVSD.py /tests/TestConnectVSD.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/vsdConnect/master/tests/TestConnectVSD.pyc -------------------------------------------------------------------------------- /tests/resources/image.nii: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/vsdConnect/master/tests/resources/image.nii -------------------------------------------------------------------------------- /tests/resources/image.xyz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loli/vsdConnect/master/tests/resources/image.xyz -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | tobiasgass 2 | Oskar Maier 3 | - changed error handling to throw exceptions 4 | - removed interactive user/pwd query from class 5 | - added method docstrings (numpy/scipy style) 6 | - started writing nose unit tests 7 | - added some !TODO items 8 | - cleaned code 9 | - changed string concatenation to new / Python3 compatible "".format() 10 | - changed prints to use the python logging module instead 11 | - added various spaces 12 | Planned: 13 | - change from urrlib2 to requests module 14 | -------------------------------------------------------------------------------- /examples/getVSDFolderIDs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import connectVSD 4 | import sys 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description='Download original image files from SMIR to a specific folder.') 8 | 9 | 10 | args=parser.parse_args() 11 | #con=connectVSD.VSDConnecter("username","password") 12 | #con=connectVSD.VSDConnecter() 13 | con=connectVSD.VSDConnecter("ZGVtb0B2aXJ0dWFsc2tlbGV0b24uY2g6ZGVtbw==") 14 | con.seturl("https://demo.virtualskeleton.ch/api/") 15 | 16 | print "Retrieving folder list from SMIR.." 17 | folderList=con.getFolderList() 18 | 19 | 20 | folderHash=con.readFolders(folderList) 21 | 22 | for folderKey in folderHash: 23 | folder=folderHash[folderKey] 24 | print folder.name, folder.ID, folder.parentFolder 25 | 26 | 27 | #con.downloadFile(56738) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README # 2 | 3 | This library implements a client for the REST API of the virtualskeletondatabase (www.virtualskeleton.ch). It supports authentication, general queries, and specific requests such as image upload/download, object linking and right management. Examples are provided in the examples directory. Please use 'demo.virtualskeleton.ch' for testing purposes. 4 | 5 | ### What is this repository for? ### 6 | 7 | * Quick summary: connect to vsd 8 | * Version: 0.1 9 | 10 | ### How do I get set up? ### 11 | 12 | Just add the source directory to your PYTHONPATH 13 | 14 | ### Contribution guidelines ### 15 | 16 | * Writing tests 17 | * Code review 18 | * Adding sockets/timeouts/retries 19 | * Adding more stable support for pagination 20 | * Add general file upload 21 | * Write some sort of GUI example 22 | 23 | ### Who do I talk to? ### 24 | 25 | * Repo owner or admin 26 | * Other community or team contact -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2015, Tobias Gass (tobiasgass@gmail.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /examples/uploadAndLinkImages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import connectVSD 4 | import sys 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description='Upload segmentation files to the SMIR to a specific folder, optionally link to source image and create ontology based on a reference segmentation object.') 8 | parser.add_argument('--targetFolderID', dest='targetFolder', default="./", required=0, 9 | help='ID of folder to store images in') 10 | parser.add_argument('--id', dest='ID', default="./",required=1, 11 | help='VSD object ID of original image ') 12 | parser.add_argument('--referenceSegID', dest='referenceSegID', default="./",required=0, 13 | help='VSD object ID of reference segmentation object for passing the ontology ') 14 | parser.add_argument('--file', dest='filename', default="./",required=1, 15 | help='filename of segmentation to upload') 16 | 17 | 18 | args=parser.parse_args() 19 | con=connectVSD.VSDConnecter("username","password") 20 | #con=connectVSD.VSDConnecter() 21 | 22 | segmentationOfOriginalObject=con.getLinkedSegmentation(args.ID) 23 | if (segmentationOfOriginalObject!=None): 24 | print "Segmentation of image already exists, aborting" 25 | sys.exit(0) 26 | 27 | 28 | print "Uploading ",args.filename 29 | segID=con.uploadSegmentation(int(args.ID),args.filename) 30 | con.addLink(segID,args.ID) 31 | print "done" 32 | 33 | if args.referenceSegID==None: 34 | args.referenceSegID=con.getLinkedSegmentation(args.ID) 35 | if args.referenceSegID==None: 36 | print "No reference seg ID was given on command line or could be found in the database, skipping ontology setting" 37 | else: 38 | con.setOntologyBasedOnReferenceObject(segID,args.referenceSegID) 39 | 40 | if (args.targetFolder != None): 41 | print "Copying file to target folder" 42 | con.addFileToFolder(segID,int(args.targetFolder)) 43 | 44 | #print "Setting permissions based on reference" 45 | #con.setRightsBasedOnReferenceObject(segID,args.ID) 46 | sys.exit() 47 | -------------------------------------------------------------------------------- /examples/downloadImages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import connectVSD 4 | import sys 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description='Download original image files from SMIR to a specific folder.') 8 | parser.add_argument('--targetFolder', dest='targetFolder', default="./", 9 | help='folder to store images in') 10 | parser.add_argument('--sourceProject', dest='targetProject', required=0, 11 | help='Project/Folder Name of VSD (sub folder of SSMProjects)') 12 | parser.add_argument('--sourceFolderID', dest='sourceFolderID', required=0, 13 | help='VSD ID of fodler to download') 14 | parser.add_argument('--sourceFolderName', dest='sourceFolderName', required=0, 15 | help='Folder name of folder to download, must be unique, can contain parentfolders, does not need to be complete') 16 | 17 | 18 | 19 | args=parser.parse_args() 20 | 21 | if (args.targetProject=="" and args.sourceFolderID=="" and args.sourceFolderName==""): 22 | print "Arguments incomplete, need either ID or name of VSD folder" 23 | sys.exit() 24 | 25 | #con=connectVSD.VSDConnecter("username","password") 26 | #con=connectVSD.VSDConnecter() 27 | 28 | 29 | 30 | print "Arguments:",args 31 | if args.sourceFolderID==None: 32 | #get information of all folders and search for the correct one 33 | print "Retrieving folder list from SMIR.." 34 | folderList=con.getFolderList() 35 | folderHash=con.readFolders(folderList) 36 | OriginalFolder=None 37 | if (args.targetProject != ""): 38 | targetProject=args.targetProject 39 | 40 | 41 | print "Retrieving target folder IDs from folder list." 42 | searchstring="SSMPipeline/"+args.targetProject+"/01_Original" 43 | for key,folder in folderHash.iteritems(): 44 | if searchstring in folder.fullName: 45 | OriginalFolder=folder 46 | else: 47 | for key,folder in folderHash.iteritems(): 48 | if args.sourceFolderName in folder.fullName: 49 | OriginalFolder=folder 50 | if OriginalFolder==None: 51 | print "Error retrieving folder, exiting" 52 | sys.exit() 53 | print "Retrieving file list from folder with ID", OriginalFolder.ID 54 | fileIDList=OriginalFolder.containedObjects.keys() 55 | else: 56 | fileList=con.getFileListInFolder(args.sourceFolderID) 57 | fileIDList=con.getFileIDs(fileList) 58 | 59 | 60 | for ID in fileIDList: 61 | print ID 62 | filename=con.generateBaseFilenameFromOntology(ID) 63 | filename=args.targetFolder+"/"+filename 64 | con.downloadFile(ID,filename) 65 | 66 | -------------------------------------------------------------------------------- /source/poster.py: -------------------------------------------------------------------------------- 1 | """Encode multipart form data to upload files via POST.""" 2 | 3 | from __future__ import print_function 4 | 5 | import mimetypes 6 | import random 7 | import string 8 | 9 | _BOUNDARY_CHARS = string.digits + string.ascii_letters 10 | 11 | def encode_multipart(fields, files, boundary=None): 12 | r"""Encode dict of form fields and dict of files as multipart/form-data. 13 | Return tuple of (body_string, headers_dict). Each value in files is a dict 14 | with required keys 'filename' and 'content', and optional 'mimetype' (if 15 | not specified, tries to guess mime type or uses 'application/octet-stream'). 16 | 17 | >>> body, headers = encode_multipart({'FIELD': 'VALUE'}, 18 | ... {'FILE': {'filename': 'F.TXT', 'content': 'CONTENT'}}, 19 | ... boundary='BOUNDARY') 20 | >>> print('\n'.join(repr(l) for l in body.split('\r\n'))) 21 | '--BOUNDARY' 22 | 'Content-Disposition: form-data; name="FIELD"' 23 | '' 24 | 'VALUE' 25 | '--BOUNDARY' 26 | 'Content-Disposition: form-data; name="FILE"; filename="F.TXT"' 27 | 'Content-Type: text/plain' 28 | '' 29 | 'CONTENT' 30 | '--BOUNDARY--' 31 | '' 32 | >>> print(sorted(headers.items())) 33 | [('Content-Length', '193'), ('Content-Type', 'multipart/form-data; boundary=BOUNDARY')] 34 | >>> len(body) 35 | 193 36 | """ 37 | def escape_quote(s): 38 | return s.replace('"', '\\"') 39 | 40 | if boundary is None: 41 | boundary = ''.join(random.choice(_BOUNDARY_CHARS) for i in range(30)) 42 | lines = [] 43 | 44 | for name, value in fields.items(): 45 | lines.extend(( 46 | '--{0}'.format(boundary), 47 | 'Content-Disposition: form-data; name="{0}"'.format(escape_quote(name)), 48 | '', 49 | str(value), 50 | )) 51 | 52 | for name, value in files.items(): 53 | filename = value['filename'] 54 | if 'mimetype' in value: 55 | mimetype = value['mimetype'] 56 | else: 57 | mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' 58 | lines.extend(( 59 | '--{0}'.format(boundary), 60 | 'Content-Disposition: form-data; name="{0}"; filename="{1}"'.format( 61 | escape_quote(name), escape_quote(filename)), 62 | 'Content-Type: {0}'.format(mimetype), 63 | '', 64 | value['content'], 65 | )) 66 | 67 | lines.extend(( 68 | '--{0}--'.format(boundary), 69 | '', 70 | )) 71 | body = '\r\n'.join(lines) 72 | 73 | headers = { 74 | 'Content-Type': 'multipart/form-data; boundary={0}'.format(boundary), 75 | 'Content-Length': str(len(body)), 76 | } 77 | 78 | return (body, headers) 79 | 80 | if __name__ == '__main__': 81 | import doctest 82 | doctest.testmod() 83 | -------------------------------------------------------------------------------- /tests/TestConnectVSD.py: -------------------------------------------------------------------------------- 1 | # nose-tests for connectVSD 0.1 2 | # (c) Oskar Maier, 2015 3 | # (c) Tobias Gass, 2015 4 | 5 | import urllib2 6 | import base64 7 | import sys 8 | 9 | from nose.tools import assert_raises 10 | 11 | import connectVSD 12 | 13 | class TestConnectVSD: 14 | 15 | __username = "demo@virtualskeleton.ch" 16 | __password = "demo" 17 | __server = "https://demo.virtualskeleton.ch/api" 18 | 19 | __testfolder = 38 # e.g. myprojects-folder 20 | 21 | __dummyusername = "user" 22 | __dummypassword = "password" 23 | __dummyserver = "http://www.google.de" 24 | 25 | __testfile = "tests/resources/image.nii" 26 | __testfile_invalid = "tests/resources/image.xyz" 27 | 28 | connection = None 29 | 30 | @classmethod 31 | def setup_class(cls): 32 | """ 33 | Set-up executed once before any test is run. 34 | """ 35 | pass 36 | 37 | def setup(self): 38 | """ 39 | Set-up executed before each test. 40 | """ 41 | self.connection = connectVSD.VSDConnecter(self.__username, self.__password) 42 | self.connection.setUrl(self.__server) 43 | 44 | def test_init(self): 45 | connection = connectVSD.VSDConnecter(self.__dummyusername, self.__dummypassword) 46 | assert connection.authstr == base64.encodestring("{}:{}".format(self.__dummyusername, self.__dummypassword)) 47 | 48 | authstr = base64.encodestring("{}:{}".format(self.__dummyusername, self.__dummypassword)) 49 | connection = connectVSD.VSDConnecter(authstr = authstr) 50 | assert connection.authstr == authstr 51 | 52 | with assert_raises(connectVSD.ConnectionException): 53 | connectVSD.VSDConnecter() 54 | 55 | with assert_raises(connectVSD.ConnectionException): 56 | connectVSD.VSDConnecter(self.__dummyusername) 57 | 58 | with assert_raises(connectVSD.ConnectionException): 59 | connectVSD.VSDConnecter(username = self.__dummyusername) 60 | 61 | with assert_raises(connectVSD.ConnectionException): 62 | connectVSD.VSDConnecter(password = self.__dummypassword) 63 | 64 | def test_setUrl(self): 65 | url = "https://just.a-test.sw" 66 | self.connection.setUrl(url) 67 | assert url == self.connection.url 68 | 69 | def test_addAuth(self): 70 | req = urllib2.Request(self.__dummyserver) 71 | req = self.connection.addAuth(req) 72 | assert req.get_header('Authorization') is not None 73 | assert self.connection.authstr in req.get_header('Authorization') 74 | 75 | def test_uploadFile(self): 76 | resp = self.connection.uploadFile(self.__testfile) 77 | assert resp is not None 78 | 79 | with assert_raises(connectVSD.RequestException): 80 | self.connection.uploadFile(self.__testfile_invalid) 81 | 82 | def test_getObject(self): 83 | resp1 = self.connection.uploadFile(self.__testfile) 84 | oid = resp1['relatedObject']['selfUrl'].split('/')[-1] 85 | resp2 = self.connection.getObject(oid) 86 | assert resp2 is not None 87 | assert int(oid) == resp2['id'] 88 | 89 | with assert_raises(connectVSD.RequestException): 90 | self.connection.getObject(sys.maxint) 91 | 92 | def test_deleteObject(self): 93 | resp1 = self.connection.uploadFile(self.__testfile) 94 | oid = resp1['relatedObject']['selfUrl'].split('/')[-1] 95 | self.connection.deleteObject(oid) 96 | 97 | with assert_raises(connectVSD.RequestException): 98 | self.connection.deleteObject(sys.maxint) 99 | 100 | def test_getFolder(self): 101 | resp = self.connection.getFolder(self.__testfolder) 102 | assert resp is not None 103 | assert self.__testfolder == resp['id'] 104 | 105 | with assert_raises(connectVSD.RequestException): 106 | self.connection.getFolder(sys.maxint) 107 | 108 | def test_addLink(self): 109 | resp1 = self.connection.uploadFile(self.__testfile) 110 | oid1 = resp1['relatedObject']['selfUrl'].split('/')[-1] 111 | resp2 = self.connection.uploadFile(self.__testfile) 112 | oid2 = resp2['relatedObject']['selfUrl'].split('/')[-1] 113 | resp3 = self.connection.addLink(oid1, oid2, description = "xxx") 114 | assert resp3 is not None 115 | 116 | 117 | -------------------------------------------------------------------------------- /source/connectVSD.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # connectVSD 0.1 4 | # (c) Tobias Gass, 2015 5 | # API documentation: https://demo.virtualskeleton.ch/api/Help/ 6 | 7 | # system imports 8 | import os 9 | import sys 10 | import urllib2 11 | import base64 12 | import json 13 | import logging 14 | 15 | # third party imports 16 | 17 | # own imports 18 | from poster import encode_multipart 19 | 20 | # code 21 | class ConnectVSDException(Exception): 22 | """Default exception for connectVSD module.""" 23 | pass 24 | 25 | class ConnectionException(ConnectVSDException): 26 | """Connection or credentials exception.""" 27 | pass 28 | 29 | class RequestException(ConnectVSDException): 30 | """Request execution exception.""" 31 | def __init__(self, message, errors): 32 | super(RequestException, self).__init__('{} : Signaled reason: {}'.format(message, errors)) 33 | 34 | class Folder: 35 | name='' 36 | fullName='' 37 | ID='' 38 | parentFolder=None 39 | childFolders=None 40 | containedObjects=None 41 | level=0 42 | def getFullName(self): 43 | if self.fullName=='': 44 | if self.parentFolder==None: 45 | self.fullName=self.name 46 | else: 47 | self.fullName=self.parentFolder.getFullName()+"/"+self.name 48 | return self.fullName 49 | 50 | class VSDConnecter: 51 | """ 52 | Handler providing convenient methods for the Virtual Skeleton Databases REST 53 | interface. 54 | 55 | !TODO: Describe __init__() here. 56 | """ 57 | 58 | url = 'https://www.virtualskeleton.ch/api' 59 | 60 | def __init__(self, username = None, password = None, authstr = None): 61 | if not username is None and not password is None: 62 | self.authstr = base64.encodestring("{}:{}".format(username, password)) 63 | elif not authstr is None: 64 | self.authstr = authstr 65 | else: 66 | raise ConnectionException('Either username and password or the authstr parameters must be provided.') 67 | 68 | logging.info("Authorization string: {}".format(self.authstr)) 69 | 70 | ################################## MISC ################################## 71 | 72 | def setUrl(self, url): 73 | """Set the server url. 74 | 75 | Parameters 76 | ---------- 77 | url : string 78 | """ 79 | self.url = url 80 | 81 | def addAuth(self, req): 82 | """Add the authorization header to a request. 83 | 84 | Parameters 85 | ---------- 86 | req : urllib2.Request 87 | 88 | Returns 89 | ------- 90 | req : urllib2.Request 91 | The request object with added authorization header. 92 | """ 93 | req.add_header('Authorization', 'Basic {}'.format(self.authstr)) 94 | return req 95 | 96 | 97 | ################################## REST-METHODS ################################## 98 | 99 | def getBySelfUrl(self, selfurl): 100 | """Get the JSON description of a single resource. 101 | 102 | Parameters 103 | ---------- 104 | selfurl : string 105 | The selfUrl identifier. 106 | 107 | Returns 108 | ------- 109 | objjson : dict 110 | The object behind the url described by a dict constructed from 111 | the servers JSON response. 112 | """ 113 | req = urllib2.Request(selfurl) 114 | return self.__execute_request(req) 115 | 116 | def getObject(self, oid): 117 | """Get the JSON description of a single object. 118 | 119 | Parameters 120 | ---------- 121 | oid : int 122 | The objects id. 123 | 124 | Returns 125 | ------- 126 | objjson : dict 127 | The object described by a dict constructed from the servers JSON 128 | response. 129 | """ 130 | #!TODO: What happens if the id is wrong? Will there still be some value returned. 131 | return self.getRequest('/objects/{}'.format(oid)) 132 | 133 | def uploadFile(self, filename): 134 | """Upload a file. 135 | 136 | Parameters 137 | ---------- 138 | filename : string 139 | Path to the file to upload. 140 | 141 | Returns 142 | ------- 143 | objjson : dict 144 | The server response as JSON dict. 145 | """ 146 | fields={} 147 | files={'file':{ 'filename' : filename, 'content': open(filename, "rb").read()}} 148 | data, headers = encode_multipart(fields, files) 149 | req = urllib2.Request('{}/upload'.format(self.url), data, headers) 150 | return self.__execute_request(req) 151 | 152 | def deleteObject(self, oid): 153 | """Delete an (unpublished) object. 154 | 155 | Parameters 156 | ---------- 157 | oid : int 158 | The objects id. 159 | """ 160 | return self.deleteRequest('/objects/{}'.format(oid)) 161 | 162 | def getFolder(self, fid): 163 | """Get the JSON description of a single object. 164 | 165 | Parameters 166 | ---------- 167 | fid : int 168 | The folders id. 169 | 170 | Returns 171 | ------- 172 | Returns 173 | ------- 174 | objjson : dict 175 | The folder described by a dict constructed from the servers JSON 176 | response. 177 | """ 178 | return self.getRequest('/folders/{}'.format(fid)) 179 | 180 | def addObjectToFolder(self, oid, fid): 181 | """Add an object to an folder. 182 | 183 | Parameters 184 | ---------- 185 | oid : int 186 | The objects id. 187 | fid : int 188 | The folders id. 189 | 190 | Returns 191 | ------- 192 | objjson : dict 193 | The server response as JSON dict. 194 | """ 195 | folder = self.getFolder(fid) 196 | entry = {'selfUrl': '{}/objects/{}'.format(fid, oid)} 197 | if folder['containedObjects'] is not None: 198 | folder['containedObjects'].append(entry) 199 | else: 200 | folder['containedObjects'] = [entry] 201 | return self.putRequest('/folders', json.dumps(folder)) 202 | 203 | def addOntology(self, oid, ontotype, ontoid): 204 | """Add an ontoloy to an object. 205 | 206 | Parameters 207 | ---------- 208 | oid : int 209 | The objects id. 210 | ontotype : int 211 | The ontology type. 212 | ontoid : int 213 | The ontology id. 214 | 215 | Returns 216 | ------- 217 | objjson : dict 218 | The server response as JSON dict. 219 | """ 220 | obj = self.getObject(oid) 221 | pos = 0 222 | if obj['ontologyItemRelations'] is not None: 223 | pos = len(obj['ontologyItemRelations']) 224 | 225 | onto_relation = {'position': pos, 226 | 'type': ontotype, 227 | 'object': {'selfUrl': '{}/objects/{}'.format(self.url, oid)}, 228 | 'ontologyItem': {'selfUrl': '{}/ontologies/{}/{}'.format(self.url, ontotype, ontoid)}} 229 | return self.postRequest('/object-ontologies/{}'.format(ontotype), json.dumps(onto_relation)) 230 | 231 | def addLink(self, oid1, oid2, description = ""): # !TODO: description is accepted, but does not seem to be settable 232 | """Create a link between two objects. 233 | 234 | Parameters 235 | ---------- 236 | oid1 : int 237 | The first objects id. 238 | oid2 : int 239 | The second objects id. 240 | description : string 241 | Optional description for the link. 242 | 243 | Returns 244 | ------- 245 | Returns 246 | ------- 247 | objlinksjson : dict 248 | The server response as JSON dict. 249 | """ 250 | link = {'object1': {'selfUrl': '{}/objects/{}'.format(self.url, oid1)}, 251 | 'object2': {'selfUrl': '{}/objects/{}'.format(self.url, oid2)}, 252 | 'description': description} 253 | return self.postRequest('/object-links', json.dumps(link)) 254 | 255 | def generateBaseFilenameFromOntology(self,ID,prefix=""): 256 | fileObject=self.getObject(ID) 257 | filename=prefix 258 | for ont in fileObject['ontologyItems']: 259 | ontology=self.getObjectByUrl(ont['selfUrl']) 260 | print ontology['term'] 261 | filename+=ontology['term'].replace(" ","_") 262 | if filename!="": 263 | filename+="-" 264 | filename+=str(ID) 265 | return filename 266 | 267 | 268 | def downloadFile(self,ID,filename,dryRun=False): 269 | d = os.path.dirname(filename) 270 | if not os.path.exists(d): 271 | os.makedirs(d) 272 | 273 | fileObject=self.getObject(ID) 274 | 275 | 276 | #return filename 277 | #if (fileObject['type']==1): 278 | if (len(fileObject['files'])>1): 279 | #DICOM 280 | ##create directory 281 | filename+="/"+os.path.basename(filename) 282 | d = os.path.dirname(filename) 283 | if not os.path.exists(d): 284 | os.makedirs(d) 285 | 286 | count=0 287 | if fileObject['name']!=None: 288 | extension=fileObject['name'].split(".")[-1] 289 | else: 290 | extension="dcm" 291 | for ffile in fileObject['files']: 292 | req=urllib2.Request(ffile['selfUrl']+"/download") 293 | self.addAuth(req) 294 | sfilename=filename+"_"+str(count)+"."+extension 295 | if not os.path.exists(sfilename): 296 | print "Downloading",ffile['selfUrl']+"/download","to",sfilename 297 | if not dryRun: 298 | response="" 299 | try: 300 | response=urllib2.urlopen(req) 301 | except urllib2.URLError as err: 302 | print "Error downloading file",ffile['selfUrl'],err 303 | sys.exit() 304 | 305 | local_file = open(sfilename, "wb") 306 | local_file.write(response.read()) 307 | local_file.close() 308 | else: 309 | print "File",sfilename,"already exists, skipping" 310 | count+=1 311 | else: 312 | #SINGLE FILE 313 | #get actual file object 314 | 315 | fileObj=self.getObjectByUrl( fileObject['files'][0]['selfUrl']) 316 | #print fileObject['id'] 317 | #print fileObj['id'] 318 | if fileObject['name']!=None: 319 | extension=fileObject['name'].split(".")[-1] 320 | else: 321 | extension="nii" 322 | sfilename=filename+"."+extension 323 | if not os.path.exists(sfilename): 324 | req=urllib2.Request(fileObj['downloadUrl']) #self.url+"/files/"+str(ID)+"/download") 325 | print "Downloading",fileObj['downloadUrl'],"to",sfilename 326 | self.addAuth(req) 327 | response="" 328 | if not dryRun: 329 | try: 330 | response=urllib2.urlopen(req) 331 | except urllib2.URLError as err: 332 | print "Error downloading file",ffile['selfUrl'],err 333 | sys.exit() 334 | 335 | local_file = open(sfilename, "wb") 336 | local_file.write(response.read()) 337 | local_file.close() 338 | 339 | 340 | 341 | 342 | 343 | ##read folder list into linked Folder datastructure 344 | def readFolders(self,folderList): 345 | #first pass: create one entry for each folder: 346 | folderHash={} 347 | for folder in folderList['items']: 348 | ID=folder['id'] 349 | folderHash[ID]=Folder() 350 | folderHash[ID].ID=ID 351 | folderHash[ID].name=folder['name'] 352 | folderHash[ID].childFolders=[] 353 | 354 | #second pass: create references to parent and child folders 355 | for folder in folderList['items']: 356 | ID=folder['id'] 357 | if (folder['childFolders']!=None): 358 | #print folder['childFolders'],ID 359 | for child in folder['childFolders']: 360 | childID=int(child['selfUrl'].split("/")[-1]) 361 | if (folderHash.has_key(childID)): 362 | folderHash[ID].childFolders.append(folderHash[childID]) 363 | if (folder['parentFolder']!=None): 364 | parentID=int(folder['parentFolder']['selfUrl'].split("/")[-1]) 365 | if (folderHash.has_key(parentID)): 366 | folderHash[ID].parentFolder=folderHash[parentID] 367 | if (not folder['containedObjects']==None): 368 | folderHash[ID].containedObjects={} 369 | for obj in folder['containedObjects']: 370 | objID=obj['selfUrl'].split("/")[-1] 371 | folderHash[ID].containedObjects[objID]=obj['selfUrl'] 372 | 373 | #third pass: gett full path names in folder hierarchy 374 | for key, folder in folderHash.iteritems(): 375 | folder.getFullName() 376 | 377 | return folderHash 378 | 379 | def getFileIDs(self,fileList): 380 | fileIDList=[] 381 | for fileObject in fileList['containedObjects']: 382 | ID=fileObject['selfUrl'].split("/")[-1] 383 | fileIDList.append(ID) 384 | return fileIDList 385 | 386 | 387 | 388 | def getLinkedSegmentation(self,objectID): 389 | result=None 390 | obj=self.getObject(objectID) 391 | for link in obj['linkedObjects']: 392 | linkedObject=self.getObjectByUrl(link['selfUrl']) 393 | if linkedObject['type']==2: 394 | result=linkedObject['id'] 395 | return result 396 | 397 | 398 | def uploadSegmentation(self,segmentationFilename): 399 | 400 | #upload Segmentation and get ID 401 | segFile=self.uploadFile(segmentationFilename) 402 | print segFile 403 | print 404 | print 405 | segObjID=int(segFile["relatedObject"]["selfUrl"].split("/")[-1]) 406 | segObj=self.getObject(segObjID) 407 | print segObj 408 | print 409 | print 410 | #check if object is segmentation 411 | maxTries=3 412 | found=0 413 | for i in range(maxTries): 414 | if segObj['type']==2 and segObj['files'][0]['selfUrl']==segFile['file']['selfUrl']: 415 | print "Found segmentation object with ID",segObj["id"] 416 | found=1 417 | break 418 | else: 419 | segObjID+=1 420 | segObj=self.getObject(segObjID) 421 | 422 | if found==0: 423 | print "Error retrieving segmentation object after upload, aborting" 424 | sys.exit(0) 425 | return segObjID 426 | 427 | def setOntologyBasedOnReferenceObject(self,targetObjectID, origObjectID): 428 | origObject=self.getObject(origObjectID) 429 | 430 | #add Ontology relations 431 | for ontRel in origObject['ontologyItemRelations']: 432 | #print "retrieving ontolgy for relation ",ontRel 433 | #print 434 | ont=self.getObjectByUrl(ontRel['selfUrl']) 435 | #print "found ontology relation:",ont 436 | #print 437 | newOntRel={} 438 | newOntRel["object"]={"selfUrl":self.url+'/objects/'+str(targetObjectID)} 439 | newOntRel["type"]=ont["type"] 440 | newOntRel["position"]=ont["position"] 441 | newOntRel["ontologyItem"]=ont["ontologyItem"] 442 | #print "Updated ontology to reflect segmentation object:",newOntRel 443 | #print 444 | #print "Uploading Ontology" 445 | result=self.addOntologyRelation(newOntRel) 446 | #print "done, result:",result 447 | #print 448 | #print 449 | #print 450 | 451 | 452 | def setRightsBasedOnReferenceObject(self,objectID,referenceObjectID): 453 | #get reference object 454 | referenceObject=self.getObject(referenceObjectID) 455 | 456 | #set group rights 457 | print "Setting group rights" 458 | if referenceObject['objectGroupRights'] is not None: 459 | for right in referenceObject['objectGroupRights']: 460 | #get object 461 | rightObject=self.getObjectByUrl(right["selfUrl"]) 462 | #create new right with the correct objectID 463 | newRight={} 464 | newRight["relatedRights"]=rightObject["relatedRights"] 465 | newRight["relatedGroup"]=rightObject["relatedGroup"] 466 | newRight["relatedObject"]={"selfUrl":self.url+"/objects/"+str(objectID)} 467 | self.postRequest("/object-group-rights",json.dumps(newRight)) 468 | 469 | #set user rights 470 | print "Setting user rights" 471 | if referenceObject['objectUserRights'] is not None: 472 | 473 | for right in referenceObject['objectUserRights']: 474 | #get object 475 | rightObject=self.getObjectByUrl(right["selfUrl"]) 476 | #create new right with the correct objectID 477 | newRight={} 478 | newRight["relatedRights"]=rightObject["relatedRights"] 479 | newRight["relatedUser"]=rightObject["relatedUser"] 480 | newRight["relatedObject"]={"selfUrl":self.url+"/objects"+str(objectID)} 481 | self.postRequest("/object-user-rights",json.dumps(newRight)) 482 | 483 | 484 | ################################## REQUESTS ################################## 485 | 486 | def getRequest(self, request): 487 | """Execute a single GET request on the server. 488 | 489 | Parameters 490 | ---------- 491 | request : string 492 | 493 | Returns 494 | ------- 495 | response : dict 496 | The server response interpreted as JSON object. 497 | """ 498 | req = urllib2.Request('{}/{}'.format(self.url, request)) 499 | return self.__execute_request(req) 500 | 501 | def optionsRequest(self, request): 502 | """Execute a single OPTIONS request on the server. 503 | 504 | Parameters 505 | ---------- 506 | request : string 507 | 508 | Returns 509 | ------- 510 | response : dict 511 | The server response interpreted as JSON object. 512 | """ 513 | req = urllib2.Request('{}/{}'.format(self.url, request)) 514 | req.get_method = lambda: 'OPTIONS' 515 | return self.__execute_request(req) 516 | 517 | def postRequest(self, request, data): 518 | """Execute a single POST request on the server. 519 | 520 | Parameters 521 | ---------- 522 | request : string 523 | data : string 524 | A string containing data in JSON format. 525 | 526 | Returns 527 | ------- 528 | response : dict 529 | The server response interpreted as JSON object. 530 | """ 531 | req = urllib2.Request('{}/{}'.format(self.url, request), 532 | data, headers={'Content-Type': 'application/json'}) 533 | req.get_method = lambda: 'POST' 534 | return self.__execute_request(req) 535 | 536 | def putRequest(self, request, data = None): 537 | """Execute a single PUT request on the server. 538 | 539 | Parameters 540 | ---------- 541 | request : string 542 | data : string 543 | A string containing data in JSON format. 544 | 545 | Returns 546 | ------- 547 | response : dict 548 | The server response interpreted as JSON object. 549 | """ 550 | if data is None: 551 | req = urllib2.Request('{}/{}'.format(self.url, request)) 552 | else: 553 | req = urllib2.Request('{}/{}'.format(self.url, request), 554 | data, headers={'Content-Type': 'application/json'}) 555 | req.get_method = lambda: 'PUT' 556 | return self.__execute_request(req) 557 | 558 | def deleteRequest(self, request): 559 | """Execute a single DELETE request on the server. 560 | 561 | Parameters 562 | ---------- 563 | request : string 564 | """ 565 | req = urllib2.Request('{}/{}'.format(self.url, request)) 566 | req.get_method = lambda: 'DELETE' 567 | self.__execute_request(req, return_json = False) 568 | 569 | def __execute_request(self, req, return_json = True): 570 | """Send a request to the server.""" 571 | self.addAuth(req) 572 | try: 573 | result = urllib2.urlopen(req) 574 | if return_json: 575 | return json.load(result) 576 | else: 577 | return 578 | except urllib2.URLError as err: 579 | raise RequestException('Error executing {} request {}'.format(req.get_method(), req.get_full_url()), err) 580 | 581 | 582 | 583 | 584 | --------------------------------------------------------------------------------