slicerNetwork.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. import qt
  2. import urllib2
  3. import socket
  4. import ssl
  5. import cookielib
  6. import xml.etree.ElementTree as ET
  7. import re
  8. import StringIO
  9. import slicer
  10. import shutil
  11. import distutils
  12. import os
  13. import base64
  14. import json
  15. #see https://gist.github.com/logic/2715756, allow requests to do Put
  16. class MethodRequest(urllib2.Request):
  17. def __init__(self, *args, **kwargs):
  18. if 'method' in kwargs:
  19. self._method = kwargs['method']
  20. del kwargs['method']
  21. else:
  22. self._method = None
  23. return urllib2.Request.__init__(self, *args, **kwargs)
  24. def get_method(self, *args, **kwargs):
  25. if self._method is not None:
  26. return self._method
  27. return urllib2.Request.get_method(self, *args, **kwargs)
  28. class ConnectionError(Exception):
  29. def __init__(self,message):
  30. self.message=message
  31. class slicerNetwork(slicer.ScriptedLoadableModule.ScriptedLoadableModule):
  32. def __init__(self, parent):
  33. slicer.ScriptedLoadableModule.ScriptedLoadableModule.__init__(self,parent)
  34. self.parent.title="slicerNetwork"
  35. pass
  36. class labkeyURIHandler(slicer.vtkURIHandler):
  37. def __init__(self):
  38. slicer.vtkURIHandler.__init__(self)
  39. self.className="labkeyURIHandler"
  40. slicer.mrmlScene.AddURIHandler(self)
  41. try:
  42. fhome=os.environ["HOME"]
  43. except:
  44. #in windows, the variable is called HOMEPATH
  45. fhome=os.environ['HOMEDRIVE']+os.environ['HOMEPATH']
  46. self.localCacheDirectory=os.path.join(fhome,"labkeyCache")
  47. self.configDir=os.path.join(fhome,".labkey")
  48. self.mode="http"
  49. #try initializing network from default config file, if found
  50. self.initFromConfig()
  51. def CanHandleURI(self,uri):
  52. print("labkeyURIHandler::CanHandleURI({0})").format(uri)
  53. if uri.find('labkey://')==0:
  54. return 1
  55. return 0
  56. def GetClassName(self):
  57. return self.className
  58. def GetHostName(self):
  59. return self.hostname
  60. def SetHostName(self,host):
  61. self.hostname=host
  62. def SetLocalCahceDirectory(self,dir):
  63. self.localCacheDirectory=dir
  64. def GetLocalCacheDirectory(self):
  65. return self.localCacheDirectory
  66. def GetLabkeyUrl(self):
  67. return self.hostname+"/labkey"
  68. def GetLabkeyWebdavUrl(self):
  69. return self.GetLabkeyUrl()+"/_webdav"
  70. #configuration part
  71. def configureSSL(self,cert,key,pwd,cacert):
  72. #do this first
  73. try:
  74. self.ctx=ssl.SSLContext(ssl.PROTOCOL_SSLv23)
  75. self.ctx.load_cert_chain(cert,keyfile=key,password=pwd)
  76. self.ctx.verify_mode=ssl.CERT_REQUIRED
  77. self.ctx.load_verify_locations(cacert)
  78. except ssl.SSLError as err:
  79. print(" Failed to configure SSL: {0}").format(str(err))
  80. self.mode="https"
  81. def initRemote(self):
  82. if self.mode=="https":
  83. http_handler=urllib2.HTTPSHandler(context=self.ctx)
  84. if self.mode=="http":
  85. http_handler=urllib2.HTTPHandler()
  86. #cookie part
  87. cj=cookielib.CookieJar()
  88. cookie_handler=urllib2.HTTPCookieProcessor(cj)
  89. self.opener=urllib2.build_opener(http_handler,cookie_handler)
  90. def initFromConfig(self):
  91. path=os.path.join(self.configDir,"Remote.json")
  92. try:
  93. self.parseConfig(path)
  94. except OSError:
  95. return
  96. self.initRemote()
  97. def parseConfig(self,fname,parent=None):
  98. try:
  99. f=open(fname)
  100. except OSError as e:
  101. print("Confgiuration error: OS error({0}): {1}").format(e.errno, e.strerror)
  102. raise
  103. dt=json.load(f)
  104. self.mode="http"
  105. if dt.has_key('SSL'):
  106. self.configureSSL(
  107. dt['SSL']['user'],
  108. dt['SSL']['key'],
  109. dt['SSL']['keyPwd'],
  110. dt['SSL']['ca']
  111. )
  112. if not parent==None:
  113. parent.userCertButton.setText(dt['SSL']['user'])
  114. parent.caCertButton.setText(dt['SSL']['ca'])
  115. parent.privateKeyButton.setText(dt['SSL']['key'])
  116. parent.pwd=dt['SSL']['keyPwd']
  117. self.hostname=dt['host']
  118. self.auth_name=dt['labkey']['user']
  119. self.auth_pass=dt['labkey']['password']
  120. if not parent==None:
  121. parent.serverURL.setText(dt['host'])
  122. parent.authName.setText(dt['labkey']['user'])
  123. parent.authPass.setText(dt['labkey']['password'])
  124. #path convention
  125. #localPath is a path on local filesystem. It means it has to adhere to OS
  126. #policy, especially in separators
  127. #relativePath is just the portion of the path that is equal both locally
  128. #and remotely. In relativePath, separator is according to http convention,
  129. #which equals separator in osx and *nix
  130. #labkeyPath or remotePath is the full URL to the resource,
  131. #including http-like header and all intermediate portions
  132. #the following functions convert between different notations
  133. #was GetLocalPath
  134. def GetLocalPathFromRelativePath(self,relativePath):
  135. debug=False
  136. relativePath=re.sub('labkey://','',relativePath)
  137. sp=os.sep.encode('string-escape')
  138. if debug:
  139. print("Substituting / with {0} in {1}").format(sp,relativePath)
  140. relativePath=re.sub('/',sp,relativePath)
  141. return os.path.join(self.GetLocalCacheDirectory(),relativePath)
  142. #was GetRemotePath
  143. def GetLabkeyPathFromRelativePath(self,source):
  144. return self.GetLabkeyWebdavUrl()+"/"+source
  145. #was GetLabkeyPathFromLocalPath
  146. def GetRelativePathFromLocalPath(self,f):
  147. debug=False
  148. #report it with URL separator, forward-slash
  149. if f.find(self.localCacheDirectory)<-1:
  150. print("Localpath misformation. Exiting")
  151. return "NULL"
  152. f0=re.sub('\\\\','\\\\\\\\',self.localCacheDirectory)
  153. relativePath=re.sub(re.compile(f0),'',f)
  154. if debug:
  155. print("[SEP] Relative path {}").format(relativePath)
  156. #leaves /ContextPath/%40files/subdirectory_list/files
  157. #remove first separator
  158. sp=os.path.sep
  159. f1=re.sub('\\\\','\\\\\\\\',sp)
  160. if relativePath[0]==sp:
  161. relativePath=relativePath[1:len(relativePath)]
  162. if debug:
  163. print("[SLASH] Relative path {}").format(relativePath)
  164. return re.sub(f1,'/',relativePath)
  165. #was GetLabkeyPathFromRemotePath
  166. def GetRelativePathFromLabkeyPath(self,f):
  167. #used to query labkey stuff, so URL separator is used
  168. #in old conventions, labkey://was used to tag a labkeyPath
  169. f=re.sub('labkey://','',f)
  170. f=re.sub(self.GetLabkeyWebdavUrl(),'',f)
  171. if f[0]=='/':
  172. f=f[1:len(f)]
  173. return f;
  174. #standard HTTP
  175. def get(self,url):
  176. debug=False
  177. if debug:
  178. print("GET: {0}").format(url)
  179. print("as {0}").format(self.auth_name)
  180. r=urllib2.Request(url)
  181. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  182. r.add_header("Authorization", "Basic %s" % base64string)
  183. try:
  184. return self.opener.open(r)
  185. #f contains json as a return value
  186. except urllib2.HTTPError as e:
  187. print e.code
  188. print e.read()
  189. return e
  190. except urllib2.URLError as e:
  191. print("Handling URLError")
  192. print e
  193. return None
  194. #a HEAD request
  195. def head(self,url):
  196. debug=False
  197. if debug:
  198. print("HEAD: {0}").format(url)
  199. print("as {0}").format(self.auth_name)
  200. r=MethodRequest(url.encode('utf-8'),method="HEAD")
  201. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  202. r.add_header("Authorization", "Basic %s" % base64string)
  203. try:
  204. return self.opener.open(r)
  205. #f contains json as a return value
  206. except urllib2.HTTPError as e:
  207. print e.code
  208. print e.read()
  209. return e
  210. def post(self,url,data):
  211. debug=False
  212. r=urllib2.Request(url)
  213. #makes it a post
  214. r.add_data(data)
  215. r.add_header("Content-Type","application/json")
  216. #add csrf
  217. csrf=self.getCSRF()
  218. r.add_header("X-LABKEY-CSRF",csrf)
  219. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  220. r.add_header("Authorization", "Basic %s" % base64string)
  221. if debug:
  222. print("{}: {}").format(r.get_method(),r.get_full_url())
  223. print("data: {}").format(r.get_data())
  224. print("Content-Type: {}").format(r.get_header('Content-Type'))
  225. try:
  226. return self.opener.open(r)
  227. except urllib2.HTTPError as e:
  228. print e.code
  229. print e.read()
  230. return e
  231. #f contains json as a return value
  232. def put(self,url,data):
  233. debug=False
  234. if debug:
  235. print("PUT: {}").format(url)
  236. r=MethodRequest(url.encode('utf-8'),method="PUT")
  237. #makes it a post
  238. r.add_data(data)
  239. print("PUT: data size: {}").format(len(data))
  240. r.add_header("content-type","application/octet-stream")
  241. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  242. r.add_header("Authorization", "Basic %s" % base64string)
  243. return self.opener.open(r)
  244. #f contains json as a return value
  245. def getCSRF(self):
  246. url=self.GetLabkeyUrl()+'/login/whoAmI.view'
  247. try:
  248. jsonData=json.load(self.get(url))
  249. except AttributeError:
  250. print("Failed")
  251. return None
  252. return jsonData["CSRF"]
  253. #file manipulation routiens
  254. #was GetFile
  255. def DownloadFileToCache(self,relativePath):
  256. debug=False
  257. # check for file in cache. If available, use, if not, download
  258. localPath=self.GetLocalPathFromRelativePath(relativePath)
  259. if os.path.isfile(localPath):
  260. if debug:
  261. print("Returning localFile {}".format(localPath))
  262. return localPath
  263. if debug:
  264. print("labkeyURIHandler::DownloadFileToCache({0}->{1})").format(relativePath,localPath)
  265. #make all necessary directories LOCALLY
  266. path=os.path.dirname(localPath)
  267. if debug:
  268. print("Target directory: '{}''".format(path))
  269. if not os.path.isdir(path):
  270. os.makedirs(path)
  271. localBuffer=open(localPath,'wb')
  272. #make sure we are at the begining of the file
  273. #labkeyPath=self.GetLabkeyPathFromRelativePath(relativePath)
  274. remoteBuffer=self.readFileToBuffer(relativePath)
  275. #check file size
  276. if debug:
  277. remoteBuffer.seek(0,2)
  278. sz=remoteBuffer.tell()
  279. print("Remote size: {0}").format(sz)
  280. remoteBuffer.seek(0)
  281. shutil.copyfileobj(remoteBuffer,localBuffer)
  282. if debug:
  283. print("Local size: {0}").format(localBuffer.tell())
  284. localBuffer.close()
  285. return localPath
  286. def fileTypesAvailable(self):
  287. return ('VolumeFile','SegmentationFile','TransformFile')
  288. #mimic slicer.util.loadNodeFromFile
  289. def loadNode(self, relativeName, filetype, properties={},returnNode=False, keepCached=True):
  290. #this is the only relevant part - file must be downloaded to cache
  291. #labkeyName is just the relative part (from labkey onwards)
  292. localPath=self.DownloadFileToCache(relativeName)
  293. if not returnNode:
  294. slicer.util.loadNodeFromFile(localPath,filetype,properties,returnNode=False)
  295. if not keepCached:
  296. os.remove(localPath)
  297. return
  298. ok,node=slicer.util.loadNodeFromFile(localPath,filetype,properties,returnNode=True)
  299. if ok:
  300. if not keepCached:
  301. os.remove(localPath)
  302. return node
  303. os.remove(localPath)
  304. return None
  305. # #remove retrieved file
  306. def storeNode(self,node,project,dir=None):
  307. removeFile=True
  308. relativePath=project+'/%40files'
  309. if not dir==None:
  310. relativePath+='/'+dir
  311. labkeyPath=self.GetLabkeyPathFromRelativePath(relativePath)
  312. print ("Remote: {}").format(labkeyPath)
  313. #checks if exists
  314. self.mkdir(labkeyPath)
  315. localPath=self.GetLocalPathFromRelativePath(relativePath)
  316. localPath.replace('/',os.path.sep)
  317. nodeName=node.GetName()
  318. suffix=".nrrd"
  319. if node.__class__.__name__=="vtkMRMLDoubleArrayNode":
  320. suffix=".mcsv"
  321. if node.__class__.__name__=="vtkMRMLTransformNode":
  322. suffix=".h5"
  323. fileName=nodeName+suffix
  324. file=os.path.join(localPath,fileName)
  325. slicer.util.saveNode(node,file)
  326. print("Stored to: {}").format(file)
  327. f=open(file,"rb")
  328. f.seek(0,2)
  329. sz=f.tell()
  330. print("File size in memory: {0}").format(sz)
  331. f.seek(0)
  332. localBuffer=f.read()
  333. print("Local buffer size: {}").format(len(localBuffer))
  334. remoteFile=labkeyPath+'/'+fileName
  335. resp=self.put(remoteFile,localBuffer)
  336. print(resp.read())
  337. f.close()
  338. if removeFile:
  339. os.remove(file)
  340. def remoteDirExists(self,url):
  341. status,dirs=self.listRemoteDir(url);
  342. return status
  343. def mkdir(self,remoteDir):
  344. if self.remoteDirExists(remoteDir):
  345. return False
  346. r=MethodRequest(remoteDir,method="MKCOL")
  347. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  348. r.add_header("Authorization", "Basic %s" % base64string)
  349. try:
  350. f=self.opener.open(r)
  351. except:
  352. print("Error: Failed MKCOL {}").format(remoteDir)
  353. return False
  354. return True
  355. def mkdirs(self,remoteDir):
  356. relativePath=self.GetRelativePathFromLabkeyPath(remoteDir)
  357. s=0
  358. while True:
  359. s1=relativePath.find('/',s)
  360. if s1<0:
  361. break
  362. path=relativePath[0:s1]
  363. remotePath=self.GetLabkeyPathFromRelativePath(relativePath)
  364. dirExists=self.remoteDirExists(remotePath)
  365. if not dirExists:
  366. if not self.mkdir(remotePath):
  367. return False
  368. s=s1+1
  369. return self.mkdir(remoteDir)
  370. def rmdir(self,remoteDir):
  371. if not self.remoteDirExists(remoteDir):
  372. return True
  373. r=MethodRequest(remoteDir,method="DELETE")
  374. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  375. r.add_header("Authorization", "Basic %s" % base64string)
  376. try:
  377. f=self.opener.open(r)
  378. except:
  379. print("Error: Failed DELETE {}").format(remoteDir)
  380. return False
  381. return True
  382. #was listDir
  383. def listRelativeDir(self,relativeDir):
  384. print("Listing for {0}").format(relativeDir)
  385. dirUrl=self.GetLabkeyPathFromRelativePath(relativeDir)
  386. status,dirs=self.listRemoteDir(dirUrl)
  387. dirs=[self.GetRelativePathFromLabkeyPath(d) for d in dirs];
  388. return dirs
  389. #was isDir
  390. def isRemoteDir(self, remotePath):
  391. #print "isDir: {}".format(remotePath)
  392. r=MethodRequest(remotePath,method="PROPFIND")
  393. PROPFIND=u"""<?xml version="1.0" encoding="utf-8"?>\n
  394. <a:propfind xmlns:a="DAV:">\n
  395. <a:prop>\n
  396. <a:resourcetype/>\n
  397. </a:prop>\n
  398. </a:propfind>"""
  399. r.add_header('content-type','text/xml; charset="utf-8"')
  400. r.add_header('content-length',str(len(PROPFIND)))
  401. r.add_header('Depth','0')
  402. r.add_data(PROPFIND)
  403. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  404. r.add_header("Authorization", "Basic %s" % base64string)
  405. print("PROPFIND: {0}").format(remotePath)
  406. try:
  407. f=self.opener.open(r)
  408. except:
  409. return False
  410. tree=ET.XML(f.read())
  411. try:
  412. rps=tree.find('{DAV:}response').find('{DAV:}propstat')
  413. rps=rps.find('{DAV:}prop')
  414. rps=rps.find('{DAV:}resourcetype').find('{DAV:}collection')
  415. if rps != None:
  416. return True
  417. else:
  418. return False
  419. except:
  420. return False
  421. def listRemoteDir(self,dirUrl):
  422. #input is remoteDir, result are remoteDirs
  423. r=MethodRequest(dirUrl,method="PROPFIND")
  424. PROPFIND=u"""<?xml version="1.0" encoding="utf-8"?>\n
  425. <propfind xmlns="DAV:">\n
  426. <prop>\n
  427. <getetag/>\n
  428. </prop>\n
  429. </propfind>"""
  430. r.add_header('content-type','text/xml; charset="utf-8"')
  431. r.add_header('content-length',str(len(PROPFIND)))
  432. r.add_header('Depth','1')
  433. r.add_data(PROPFIND)
  434. base64string = base64.b64encode('%s:%s' % (self.auth_name, self.auth_pass))
  435. r.add_header("Authorization", "Basic %s" % base64string)
  436. print("PROPFIND: {0}").format(dirUrl)
  437. dirs=[]
  438. try:
  439. f=self.opener.open(r)
  440. except:
  441. return False,dirs
  442. tree=ET.XML(f.read())
  443. rps=tree.findall('{DAV:}response')
  444. for r in rps:
  445. hr=r.find('{DAV:}href')
  446. dirent=hr.text
  447. #dirent=re.sub('/labkey/_webdav/','',dirent)
  448. dirent=self.GetHostName()+dirent
  449. dirs.append(dirent)
  450. del dirs[0]
  451. return True,dirs
  452. def toRelativePath(self,dirs):
  453. flist1=[]
  454. for d in dirs:
  455. if d[-1]=='/':
  456. d=d[:-1]
  457. if d.rfind('/')>-1:
  458. d=d[d.rfind('/')+1:]
  459. flist1.append(d)
  460. return flist1
  461. def readFileToBuffer(self, relativePath):
  462. dirUrl=self.GetLabkeyPathFromRelativePath(relativePath)
  463. f=self.get(dirUrl)
  464. return StringIO.StringIO(f.read())
  465. def uploadFile(self,localPath):
  466. #get upstream directories sorted out
  467. relativePath=self.GetRelativePathFromLocalPath(localPath)
  468. if relativePath=="NULL":
  469. errorCode="Failed to upload {}. Potential incorrect location"
  470. errorCode+=". Should be in labkeyCache!"
  471. print(errorCode.format(relativePath))
  472. return False
  473. relativeDir=relativePath[0:labkeyPath.rfind('/')]
  474. remoteDir=self.GetLabkeyPathFromRemotePath(relativeDir)
  475. if not self.remoteDirExists(remoteDir):
  476. if not self.mkdirs(remoteDir):
  477. errorCode="UploadFile: Could not create directory {}"
  478. print(errorCode.format(remoteDir))
  479. return False
  480. #make an URL request
  481. with open(localPath, 'rb') as f:
  482. data=f.read()
  483. remotePath=self.GetLabkeyPathFromRelativePath(relativePath)
  484. self.put(remotePath,data)
  485. #was copyFileToRemote
  486. def copyLocalFileToRemote(self,localPath, remotePath):
  487. #get upstream directories sorted out
  488. remoteDir=os.path.dirname(remotePath)
  489. if not self.remoteDirExists(remoteDir):
  490. if not self.mkdirs(remoteDir):
  491. errorCode="UploadFile: Could not create directory {}"
  492. print(errorCode.format(remoteDir))
  493. return False
  494. #make an URL request
  495. if (self.isRemoteDir(remotePath)):
  496. remotePath=remotePath+'/'+os.path.basename(localPath)
  497. with open(localPath, 'rb') as f:
  498. data=f.read()
  499. itry=0
  500. while itry<10:
  501. try:
  502. self.put(remotePath,data)
  503. break
  504. except (socket.error,urllib2.URLError):
  505. print("[{}] Failed to execute put(), retry".format(itry))
  506. itry=itry+1
  507. if itry==10:
  508. raise ConnectionError('Could not copy file, number of tries exceeded')
  509. #was loadDir
  510. def DownloadDirToCache(self, relativePath):
  511. files=self.listRelativeDir(relativePath)
  512. fdir="NONE"
  513. for f in files:
  514. #f is local path
  515. try:
  516. localDir=os.path.dirname(self.DownloadFileToCache(f))
  517. except:
  518. #fails if there is a subdirectory; go recursively
  519. print("self.readDir(f) not implemented")
  520. return localDir
  521. #database routines
  522. def loadDataset(self,project,dataset):
  523. url=self.GetLabkeyUrl()+'/'+project
  524. url+='/query-selectRows.api?schemaName=study&query.queryName='+dataset
  525. return json.load(self.get(url))
  526. def filterDataset(self,project,dataset,filter):
  527. debug=False
  528. url=self.GetLabkeyUrl()+'/'+project
  529. url+='/query-selectRows.api?schemaName=study&query.queryName='+dataset
  530. for f in filter:
  531. url+="&query."+f['variable']+"~"+f['oper']+"="+f['value']
  532. if debug:
  533. print("Sending {}").format(url)
  534. return json.load(self.get(url))
  535. def modifyDataset(self,method,project,dataset,rows):
  536. #method can be insert or update
  537. data={}
  538. data['schemaName']='study'
  539. data['queryName']=dataset
  540. data['rows']=rows
  541. url=self.GetLabkeyUrl()+'/'+project
  542. url+='/query-'+method+'Rows.api?'
  543. return self.post(url,json.dumps(data))
  544. def filterList(self,project,dataset,filter):
  545. schemaName='lists'
  546. debug=False
  547. url=self.GetLabkeyUrl()+'/'+project
  548. url+='/query-selectRows.api?schemaName='+schemaName+'&query.queryName='+dataset
  549. for f in filter:
  550. url+="&query."+f['variable']+"~"+f['oper']+"="+f['value']
  551. if debug:
  552. print("Sending {}").format(url)
  553. return json.load(self.get(url))
  554. def modifyList(self,method,project,dataset,rows):
  555. #method can be insert or update
  556. data={}
  557. schemaName='lists'
  558. data['schemaName']=schemaName
  559. data['queryName']=dataset
  560. data['rows']=rows
  561. url=self.GetLabkeyUrl()+'/'+project
  562. url+='/query-'+method+'Rows.api?'
  563. return self.post(url,json.dumps(data))