diff --git a/.gitignore b/.gitignore index 1e0f5da..1463c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ build eggs doc_build parts -bin var sdist /scripts diff --git a/README.md b/README.md index 07f38c6..351a126 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Mauricio Varea REQUIREMENTS ========================================= -Python 2.6 with the packages 'pycurl', and 'python-dateutil' installed. You can install these on Ubuntu with 'apt-get install python-pycurl' and 'apt-get install python-dateutil'. +Python 2.6 with the package 'python-dateutil' installed. You can install these on Ubuntu with 'apt-get install python-dateutil'. INSTALL diff --git a/doc/_update_doc/Getting Started.txt b/doc/_update_doc/Getting Started.txt index d71eb83..654e28f 100644 --- a/doc/_update_doc/Getting Started.txt +++ b/doc/_update_doc/Getting Started.txt @@ -25,7 +25,7 @@ Version 0.1 of ``BaseSpacePy`` can be checked out here: Setup ################### -*Requirements:* Python 2.6 with the packages 'urllib2', 'pycurl', 'multiprocessing' and 'shutil' available. +*Requirements:* Python 2.6 with the packages 'urllib2', 'multiprocessing' and 'shutil' available. The multi-part file upload will currently only run on a unix setup. diff --git a/doc/html/Getting Started.html b/doc/html/Getting Started.html index 522ba9a..69388ca 100644 --- a/doc/html/Getting Started.html +++ b/doc/html/Getting Started.html @@ -67,7 +67,7 @@

Availability

SetupΒΆ

-

Requirements: Python 2.6 with the packages ‘urllib2’, ‘pycurl’, ‘multiprocessing’ and ‘shutil’ available.

+

Requirements: Python 2.6 with the packages ‘urllib2’, ‘multiprocessing’ and ‘shutil’ available.

The multi-part file upload will currently only run on a unix setup.

To install ‘BaseSpacePy’ run the ‘setup.py’ script in the src directory (for a global install you will need to run this command with root privileges):

cd basespace-python-sdk/src
diff --git a/doc/html/_modules/BaseSpacePy/api/BaseSpaceAPI.html b/doc/html/_modules/BaseSpacePy/api/BaseSpaceAPI.html
index e2006c6..cbf5f83 100644
--- a/doc/html/_modules/BaseSpacePy/api/BaseSpaceAPI.html
+++ b/doc/html/_modules/BaseSpacePy/api/BaseSpaceAPI.html
@@ -50,7 +50,6 @@ 

Source code for BaseSpacePy.api.BaseSpaceAPI

import urllib2
 import shutil
 import urllib
-import pycurl
 import httplib
 import cStringIO
 import json
@@ -252,9 +251,9 @@ 

Source code for BaseSpacePy.api.BaseSpaceAPI

resourcePath = self.apiClient.apiServer + '/appsessions/{AppSessionId}'        
         resourcePath = resourcePath.replace('{AppSessionId}', Id)        
         response = cStringIO.StringIO()
-        c = pycurl.Curl()
-        c.setopt(pycurl.URL, resourcePath)
-        c.setopt(pycurl.USERPWD, self.key + ":" + self.secret)
+        c = .Curl()
+        c.setopt(.URL, resourcePath)
+        c.setopt(.USERPWD, self.key + ":" + self.secret)
         c.setopt(c.WRITEFUNCTION, response.write)
         c.perform()
         c.close()
diff --git a/doc/html/_sources/Getting Started.txt b/doc/html/_sources/Getting Started.txt
index d71eb83..654e28f 100644
--- a/doc/html/_sources/Getting Started.txt	
+++ b/doc/html/_sources/Getting Started.txt	
@@ -25,7 +25,7 @@ Version 0.1 of ``BaseSpacePy`` can be checked out here:
 Setup
 ###################
 
-*Requirements:* Python 2.6 with the packages 'urllib2', 'pycurl', 'multiprocessing' and 'shutil' available.
+*Requirements:* Python 2.6 with the packages 'urllib2', 'multiprocessing' and 'shutil' available.
 
 The multi-part file upload will currently only run on a unix setup.
 
diff --git a/doc/html/searchindex.js b/doc/html/searchindex.js
index d240da6..b6533b2 100644
--- a/doc/html/searchindex.js
+++ b/doc/html/searchindex.js
@@ -1 +1 @@
-Search.setIndex({objects:{"BaseSpacePy.model.File.File":{getIntervalCoverage:[2,0,1,""],isValidFileOption:[2,0,1,""],isInit:[2,0,1,""],downloadFile:[2,0,1,""],getVariantMeta:[2,0,1,""],getCoverageMeta:[2,0,1,""],getFileUrl:[2,0,1,""],filterVariant:[2,0,1,""],getFileS3metadata:[2,0,1,""]},"BaseSpacePy.model.QueryParameters":{QueryParameters:[2,1,1,""]},"BaseSpacePy.model.Run.Run":{isInit:[2,0,1,""],getFiles:[2,0,1,""],getSamples:[2,0,1,""],getAccessStr:[2,0,1,""]},"BaseSpacePy.api.BaseSpaceAPI.BaseSpaceAPI":{getFileById:[2,0,1,""],getAppResultFilesById:[2,0,1,""],getRunFilesById:[2,0,1,""],getUserById:[2,0,1,""],getProjectById:[2,0,1,""],filterVariantSet:[2,0,1,""],getIntervalCoverage:[2,0,1,""],getAppSession:[2,0,1,""],getAppSessionById:[2,0,1,""],getAccessibleRunsByUser:[2,0,1,""],getGenomeById:[2,0,1,""],setAppSessionState:[2,0,1,""],getWebVerificationCode:[2,0,1,""],createAppResult:[2,0,1,""],getSamplePropertiesById:[2,0,1,""],getAppSessionPropertiesById:[2,0,1,""],obtainAccessToken:[2,0,1,""],getAppSessionPropertyByName:[2,0,1,""],multipartFileDownload:[2,0,1,""],multipartFileUpload:[2,0,1,""],updatePrivileges:[2,0,1,""],getAppResultsByProject:[2,0,1,""],getAppResultFiles:[2,0,1,""],getProjectPropertiesById:[2,0,1,""],getVariantMetadata:[2,0,1,""],getRunById:[2,0,1,""],createProject:[2,0,1,""],getFilePropertiesById:[2,0,1,""],getAccess:[2,0,1,""],getFilesBySample:[2,0,1,""],getAppSessionInputsById:[2,0,1,""],getVerificationCode:[2,0,1,""],getAvailableGenomes:[2,0,1,""],getSampleById:[2,0,1,""],appResultFileUpload:[2,0,1,""],fileS3metadata:[2,0,1,""],fileDownload:[2,0,1,""],getSamplesByProject:[2,0,1,""],getCoverageMetaInfo:[2,0,1,""],getProjectByUser:[2,0,1,""],getSampleFilesById:[2,0,1,""],getAppResultById:[2,0,1,""],getRunSamplesById:[2,0,1,""],fileUrl:[2,0,1,""],getAppResultPropertiesById:[2,0,1,""],getRunPropertiesById:[2,0,1,""]},"BaseSpacePy.model.AppSession":{AppSession:[2,1,1,""]},"BaseSpacePy.model.QueryParameters.QueryParameters":{validate:[2,0,1,""]},"BaseSpacePy.model.Project":{Project:[2,1,1,""]},"BaseSpacePy.model.Sample":{Sample:[2,1,1,""]},"BaseSpacePy.model.AppResult.AppResult":{getFiles:[2,0,1,""],uploadFile:[2,0,1,""],isInit:[2,0,1,""],getReferencedSamples:[2,0,1,""],getAccessStr:[2,0,1,""],getReferencedSamplesIds:[2,0,1,""]},"BaseSpacePy.model.Run":{Run:[2,1,1,""]},"BaseSpacePy.api.BaseSpaceAPI":{BaseSpaceAPI:[2,1,1,""]},"BaseSpacePy.model.AppResult":{AppResult:[2,1,1,""]},"BaseSpacePy.model.Sample.Sample":{isInit:[2,0,1,""],getFiles:[2,0,1,""],getAccessStr:[2,0,1,""],getReferencedAppResults:[2,0,1,""]},"BaseSpacePy.model.Project.Project":{isInit:[2,0,1,""],getAppResults:[2,0,1,""],getAccessStr:[2,0,1,""],createAppResult:[2,0,1,""],getSamples:[2,0,1,""]},"BaseSpacePy.model.File":{File:[2,1,1,""]}},terms:{all:[0,2],code:[0,2],queri:[0,3,2],global:[0,3],getfilesbysampl:2,nwe:0,prefix:0,sleep:0,follow:0,getproject:0,depend:2,basespacepy_vx:0,getrunsamplesbyid:2,texliv:1,send:2,hrefcoverag:2,granular:2,present:2,sourc:2,string:[0,2],accesstoken:[0,2],fals:2,account:0,getfilepropertiesbyid:2,myprojects2:0,veri:[0,2],testfile2:0,brows:[0,3,2],getprojectbyid:[0,2],getappresultfilesbyid:2,contenttyp:2,level:2,list:[0,2],upload:[0,3,2],"try":0,item:0,getgenomebyid:[0,2],verif:[0,2],small:2,refer:2,round:2,dir:0,pleas:0,work:0,second:0,pass:2,download:[0,2],further:[0,2],cat:2,append:0,even:0,index:3,filedownload:2,compar:0,neg:2,section:0,abl:0,hrefvari:2,current:[0,2],delet:1,version:[0,1,2],"new":[0,1,2],method:[0,1,2],metadata:2,abov:0,gener:[0,1,2],here:0,shouldn:2,varmeta:0,let:0,ubuntu:0,basespaceauth:0,path:[0,2],getappsessioninputsbyid:2,sinc:1,valu:[0,2],search:3,queur:0,larger:2,prior:0,isinit:2,tauru:0,action:0,implement:2,chrchr2:0,getaccesstoken:0,privilig:0,extra:1,app:[0,2],apt:1,deprec:2,unix:[0,2],api:[0,3,2],getappsessionbyid:2,instal:[0,1],txt:[0,1],"2x26":0,cloud:0,from:[0,2],rattu:0,commun:2,visit:0,two:0,coverag:[0,2],next:0,websit:0,multipartfiledownload:2,call:[0,2],recommend:1,scope:[0,2],type:[0,2],nrun:0,more:[0,2],sort:[0,2],oauthexcept:2,desir:2,relat:[0,2],notic:0,granttyp:2,known:2,actual:2,hiseq:0,partsiz:2,must:[0,2],none:2,retriev:[0,2],pycurl:0,local:2,setup:[0,3],launch:0,getlaunchtyp:0,getfil:[0,2],getsamplepropertiesbyid:2,apporpri:0,endor:0,purpos:0,root:[0,2],norvegicu:0,nearest:2,prompt:0,stream:2,give:0,process:[0,2],accept:2,abort:2,want:0,sought:0,unknownparameterexcept:2,getrun:0,end:[0,2],applaunch:0,anoth:0,write:[0,2],how:0,regist:0,updat:[0,1,2],files3metadata:2,referenc:2,max:[0,2],clone:0,timedout:2,variant:[0,2],befor:0,mai:[0,1,2],associ:[0,2],parallel:2,demonstr:0,getfilebyid:[0,2],"short":0,attempt:2,chr2:[0,2],correspond:2,element:2,inform:[0,2],environ:0,thaliana:0,morten:0,varianthead:[0,2],mygenom:0,authorization_cod:2,help:[0,1],over:2,appsessionid:2,through:[0,2],coli:0,paramet:[0,2],alten:0,binari:2,getappresultfilebyid:2,getwebverificationcod:2,window:2,singleproject:0,pythonpath:0,local_dir:2,sapien:0,main:2,non:2,"return":2,thei:2,s_g1_l001_r2_001:0,python:[0,1],auto:1,cover:0,initi:[0,2],mybam:0,mybasespaceapi:0,now:0,introduct:[0,3],fontx:1,multiprocess:0,name:[0,2],edit:1,authent:[0,2],easili:0,token:[0,3,2],mode:2,timeout:2,genom:[0,2],debug:2,fulli:2,mean:[0,2],status:2,getreferencedappresult:2,chunk:2,hard:0,procedur:0,meta:0,expect:0,our:0,special:0,out:0,variabl:2,vcf:[0,3,2],newli:[0,1,2],getreferencedsamplesid:2,querypar:2,content:[1,3,2],uploadfil:[0,2],getprojectbyus:[0,2],etag:2,print:0,model:[3,2],after:[0,2],insid:2,situat:2,plex:0,triggerobj:0,base:[0,1,2],dictionari:2,needsattent:2,org:1,"byte":2,md5:2,thread:2,doctre:1,musculu:0,filter:[0,2],place:[0,2],isn:2,nthese:0,first:0,rang:2,directli:0,onc:[0,2],propertylist:2,number:[0,2],yourself:0,restrict:2,alreadi:[0,2],done:0,triggertyp:0,miss:2,primari:0,getfiles3metadata:2,size:2,createbspath:2,given:2,script:0,data:[0,3,2],top:0,system:2,store:2,option:[0,2],urllib2:0,specifi:[0,2],getappsesss:2,github:0,accompani:0,staphylococcu:0,than:2,endpo:2,downloadfil:[0,2],instanc:[0,2],provid:[0,2],remov:1,tree:[0,3],project:[0,3,2],str:0,posit:[0,2],multipart:2,comput:0,ana:0,fastq:0,filtervariantset:2,argument:2,client_kei:0,myproject:0,packag:0,bacillu:0,manner:0,have:[0,1,2],tabl:3,need:[0,1,2],getfileurl:2,illegalparameterexcept:2,amplicon:0,startpo:2,getrunbyid:2,getintervalcoverag:[0,2],client:[0,2],note:[0,2],also:0,chromosom:[0,2],exampl:[0,2],take:0,indic:3,singl:2,sure:[0,1],modelnotinitializedexcept:2,test:[0,2],object:[0,2],getverificationcod:[0,2],fileurl:2,"class":[0,1,2],latex:1,getsampl:[0,2],url:2,doc:1,request:[0,3,2],obtainaccesstoken:2,v1pre2:0,part:[0,2],getappresultbyid:2,getrunpropertiesbyid:2,phix:0,getrunfilesbyid:2,show:0,text:[0,2],filetyp:2,session:0,saccharomyc:0,permiss:0,redirecturl:2,cereu:0,redirect:2,access:[0,3,2],onli:[0,2],locat:0,createappresult:2,illumina:0,multivaluepropertyappresultslist:2,getappsess:2,transact:0,solut:0,state:[0,2],haven:0,dict:2,analyz:0,folder:[0,2],analys:0,get:[0,1,2,3],familiar:0,stop:2,getsamplebyid:2,chrom:2,gen:0,requir:[0,2],accesstyp:2,enabl:0,undefinedparameterexcept:2,acut:2,remot:2,allgenom:0,gran:0,privileg:0,grab:0,bam:[0,3,2],basespaceapi:[0,2],summari:[0,2],set:2,see:[0,1],result:[0,3,2],respons:2,fail:2,subject:0,statu:[0,2],getappsessionpropertybynam:2,getaccessiblerunsbyus:2,appresult:[3,2],favor:2,written:1,between:0,"import":0,attribut:2,altern:0,kei:[0,2],buckets:0,filtervari:[0,2],popul:2,verification_with_code_uri:0,last:0,cov:[0,2],samplecount:0,region:2,equal:2,createproject:2,etc:2,redirect_uri:2,pdf:1,com:0,getcoveragemeta:[0,2],nsome:0,simpli:0,coveragemetadata:2,can:[0,2],instanti:0,clientsecret:2,applicationact:0,header:2,empti:2,suppli:0,getappresultpropertiesbyid:2,assum:0,devic:[0,2],due:2,been:[0,2],getaccessstr:[0,2],secret:[0,2],trigger:[0,3],rhodobact:0,interest:2,basic:0,addit:0,clientkei:2,getprojectpropertiesbyid:2,getsamplesbyproject:2,getcoveragemetainfo:2,coordin:2,cerevisia:0,repres:2,those:2,"case":2,multi:[0,2],look:0,access_token:0,plain:[0,2],align:2,properti:2,basespac:[0,2],coveragemeta:0,"while":2,na18507:0,myvcf:0,error:2,getappresultfil:2,setappsessionst:2,howev:2,loop:0,getavailablegenom:[0,2],file:[0,3,2],site:0,getappresult:2,myapi:0,basespacetestfil:0,s_g1_l001_r2_002:0,descript:[0,2],shutil:0,multipartfileupload:2,updateprivileg:2,par:2,disabl:2,develop:[0,1],sphaeroid:0,grant:[0,2],getapptrigg:0,make:[0,1,2],belong:0,createbsdir:2,same:[0,2],setstatu:0,handl:0,mkallberg:0,html:1,"2x151":0,document:[0,1],latexpdf:1,complet:[0,2],byterang:2,http:[0,1],resequenc:0,rais:2,temporari:2,user:[0,2],client_secret:0,appsess:[3,2],chang:[0,2],expand:0,bsauth:0,well:0,without:2,command:0,thi:[0,1,2],everyth:0,identifi:0,paus:0,sample_3:0,getvariantmetadata:2,obtain:0,rest:[0,2],sample_2:0,sample_1:0,human:0,outlin:0,yet:2,web:[0,2],cut:0,easi:0,getanalys:0,point:0,except:2,device_cod:0,add:[0,1],other:2,input:2,modul:[1,3,2],match:2,applic:[0,3,2],appresultfileupload:2,which:2,format:2,read:[0,2],piec:0,getvariantmeta:[0,2],isvalidfileopt:2,getappsessionpropertiesbyid:2,desc:2,applicationactionid:0,specif:[0,2],success:2,filenam:2,should:2,server:[0,2],href:0,necessari:0,either:[0,2],localdir:2,output:0,page:3,interv:[0,2],some:0,localpath:2,intern:2,homo:0,sampl:[0,3,2],analysisfil:0,aureu:0,octet:2,achiev:0,per:2,ucsc:0,larg:2,basespacepi:[0,1,2,3],approv:[0,2],getsamplefilesbyid:2,who:0,run:[0,1,2,3],ntype:0,nmy:0,step:0,apiserv:[0,2],bolt:0,src:0,about:[0,2],obj:2,getuserbyid:[0,2],genomev1:2,hg19:0,getaccess:2,produc:0,own:0,analysis2:0,within:[0,2],automat:0,mydir:0,s_g1_l001_r1_001:0,s_g1_l001_r1_002:0,your:0,getbasespaceapi:0,git:0,byterangeexcept:2,transfer:2,support:2,json:2,custom:2,avail:[0,1,2,3],start:[0,3,2],covmeta:0,includ:[0,2],"var":0,modelnotsupportedexcept:2,individu:2,analysi:[0,3],properli:[1,2],form:2,escherichia:0,projectlist:2,yourproject:0,link:1,oauth:0,"true":[0,2],sdk:[0,1],info:0,made:[0,2],arabidopsi:0,temp:2,possibl:2,"default":2,wish:[0,1,2],maximum:2,record:2,below:0,statussummari:2,processcount:2,"export":0,getappresultsbyproject:2,basespaceurl:0,displaynam:0,creat:[0,1,2,3],flow:0,uri:[0,2],exist:[1,2],kallberg:0,ing:2,check:0,fill:0,again:0,titl:1,when:[0,2],detail:0,invalid:2,field:2,valid:[0,2],tempdir:2,you:[0,1,2],nthe:0,nproject:0,ecoli:0,clean:1,sequenc:[0,2],nafter:0,docstr:1,ngenom:0,log:0,getreferencedsampl:2,sphinx:1,faster:0,directori:[0,1,2],deviceinfo:0,ignor:2,createanalysi:0,time:0,queryparamet:[3,2],profil:2},objtypes:{"0":"py:method","1":"py:class"},titles:["Getting Started","<no title>","Available modules","BaseSpacePy"],objnames:{"0":["py","method","Python method"],"1":["py","class","Python class"]},filenames:["Getting Started","README","Available modules","index"]})
\ No newline at end of file
+Search.setIndex({objects:{"BaseSpacePy.model.File.File":{getIntervalCoverage:[2,0,1,""],isValidFileOption:[2,0,1,""],isInit:[2,0,1,""],downloadFile:[2,0,1,""],getVariantMeta:[2,0,1,""],getCoverageMeta:[2,0,1,""],getFileUrl:[2,0,1,""],filterVariant:[2,0,1,""],getFileS3metadata:[2,0,1,""]},"BaseSpacePy.model.QueryParameters":{QueryParameters:[2,1,1,""]},"BaseSpacePy.model.Run.Run":{isInit:[2,0,1,""],getFiles:[2,0,1,""],getSamples:[2,0,1,""],getAccessStr:[2,0,1,""]},"BaseSpacePy.api.BaseSpaceAPI.BaseSpaceAPI":{getFileById:[2,0,1,""],getAppResultFilesById:[2,0,1,""],getRunFilesById:[2,0,1,""],getUserById:[2,0,1,""],getProjectById:[2,0,1,""],filterVariantSet:[2,0,1,""],getIntervalCoverage:[2,0,1,""],getAppSession:[2,0,1,""],getAppSessionById:[2,0,1,""],getAccessibleRunsByUser:[2,0,1,""],getGenomeById:[2,0,1,""],setAppSessionState:[2,0,1,""],getWebVerificationCode:[2,0,1,""],createAppResult:[2,0,1,""],getSamplePropertiesById:[2,0,1,""],getAppSessionPropertiesById:[2,0,1,""],obtainAccessToken:[2,0,1,""],getAppSessionPropertyByName:[2,0,1,""],multipartFileDownload:[2,0,1,""],multipartFileUpload:[2,0,1,""],updatePrivileges:[2,0,1,""],getAppResultsByProject:[2,0,1,""],getAppResultFiles:[2,0,1,""],getProjectPropertiesById:[2,0,1,""],getVariantMetadata:[2,0,1,""],getRunById:[2,0,1,""],createProject:[2,0,1,""],getFilePropertiesById:[2,0,1,""],getAccess:[2,0,1,""],getFilesBySample:[2,0,1,""],getAppSessionInputsById:[2,0,1,""],getVerificationCode:[2,0,1,""],getAvailableGenomes:[2,0,1,""],getSampleById:[2,0,1,""],appResultFileUpload:[2,0,1,""],fileS3metadata:[2,0,1,""],fileDownload:[2,0,1,""],getSamplesByProject:[2,0,1,""],getCoverageMetaInfo:[2,0,1,""],getProjectByUser:[2,0,1,""],getSampleFilesById:[2,0,1,""],getAppResultById:[2,0,1,""],getRunSamplesById:[2,0,1,""],fileUrl:[2,0,1,""],getAppResultPropertiesById:[2,0,1,""],getRunPropertiesById:[2,0,1,""]},"BaseSpacePy.model.AppSession":{AppSession:[2,1,1,""]},"BaseSpacePy.model.QueryParameters.QueryParameters":{validate:[2,0,1,""]},"BaseSpacePy.model.Project":{Project:[2,1,1,""]},"BaseSpacePy.model.Sample":{Sample:[2,1,1,""]},"BaseSpacePy.model.AppResult.AppResult":{getFiles:[2,0,1,""],uploadFile:[2,0,1,""],isInit:[2,0,1,""],getReferencedSamples:[2,0,1,""],getAccessStr:[2,0,1,""],getReferencedSamplesIds:[2,0,1,""]},"BaseSpacePy.model.Run":{Run:[2,1,1,""]},"BaseSpacePy.api.BaseSpaceAPI":{BaseSpaceAPI:[2,1,1,""]},"BaseSpacePy.model.AppResult":{AppResult:[2,1,1,""]},"BaseSpacePy.model.Sample.Sample":{isInit:[2,0,1,""],getFiles:[2,0,1,""],getAccessStr:[2,0,1,""],getReferencedAppResults:[2,0,1,""]},"BaseSpacePy.model.Project.Project":{isInit:[2,0,1,""],getAppResults:[2,0,1,""],getAccessStr:[2,0,1,""],createAppResult:[2,0,1,""],getSamples:[2,0,1,""]},"BaseSpacePy.model.File":{File:[2,1,1,""]}},terms:{all:[0,2],code:[0,2],queri:[0,3,2],global:[0,3],getfilesbysampl:2,nwe:0,prefix:0,sleep:0,follow:0,getproject:0,depend:2,basespacepy_vx:0,getrunsamplesbyid:2,texliv:1,send:2,hrefcoverag:2,granular:2,present:2,sourc:2,string:[0,2],accesstoken:[0,2],fals:2,account:0,getfilepropertiesbyid:2,myprojects2:0,veri:[0,2],testfile2:0,brows:[0,3,2],getprojectbyid:[0,2],getappresultfilesbyid:2,contenttyp:2,level:2,list:[0,2],upload:[0,3,2],"try":0,item:0,getgenomebyid:[0,2],verif:[0,2],small:2,refer:2,round:2,dir:0,pleas:0,work:0,second:0,pass:2,download:[0,2],further:[0,2],cat:2,append:0,even:0,index:3,filedownload:2,compar:0,neg:2,section:0,abl:0,hrefvari:2,current:[0,2],delet:1,version:[0,1,2],"new":[0,1,2],method:[0,1,2],metadata:2,abov:0,gener:[0,1,2],here:0,shouldn:2,varmeta:0,let:0,ubuntu:0,basespaceauth:0,path:[0,2],getappsessioninputsbyid:2,sinc:1,valu:[0,2],search:3,queur:0,larger:2,prior:0,isinit:2,tauru:0,action:0,implement:2,chrchr2:0,getaccesstoken:0,privilig:0,extra:1,app:[0,2],apt:1,deprec:2,unix:[0,2],api:[0,3,2],getappsessionbyid:2,instal:[0,1],txt:[0,1],"2x26":0,cloud:0,from:[0,2],rattu:0,commun:2,visit:0,two:0,coverag:[0,2],next:0,websit:0,multipartfiledownload:2,call:[0,2],recommend:1,scope:[0,2],type:[0,2],nrun:0,more:[0,2],sort:[0,2],oauthexcept:2,desir:2,relat:[0,2],notic:0,granttyp:2,known:2,actual:2,hiseq:0,partsiz:2,must:[0,2],none:2,retriev:[0,2],local:2,setup:[0,3],launch:0,getlaunchtyp:0,getfil:[0,2],getsamplepropertiesbyid:2,apporpri:0,endor:0,purpos:0,root:[0,2],norvegicu:0,nearest:2,prompt:0,stream:2,give:0,process:[0,2],accept:2,abort:2,want:0,sought:0,unknownparameterexcept:2,getrun:0,end:[0,2],applaunch:0,anoth:0,write:[0,2],how:0,regist:0,updat:[0,1,2],files3metadata:2,referenc:2,max:[0,2],clone:0,timedout:2,variant:[0,2],befor:0,mai:[0,1,2],associ:[0,2],parallel:2,demonstr:0,getfilebyid:[0,2],"short":0,attempt:2,chr2:[0,2],correspond:2,element:2,inform:[0,2],environ:0,thaliana:0,morten:0,varianthead:[0,2],mygenom:0,authorization_cod:2,help:[0,1],over:2,appsessionid:2,through:[0,2],coli:0,paramet:[0,2],alten:0,binari:2,getappresultfilebyid:2,getwebverificationcod:2,window:2,singleproject:0,pythonpath:0,local_dir:2,sapien:0,main:2,non:2,"return":2,thei:2,s_g1_l001_r2_001:0,python:[0,1],auto:1,cover:0,initi:[0,2],mybam:0,mybasespaceapi:0,now:0,introduct:[0,3],fontx:1,multiprocess:0,name:[0,2],edit:1,authent:[0,2],easili:0,token:[0,3,2],mode:2,timeout:2,genom:[0,2],debug:2,fulli:2,mean:[0,2],status:2,getreferencedappresult:2,chunk:2,hard:0,procedur:0,meta:0,expect:0,our:0,special:0,out:0,variabl:2,vcf:[0,3,2],newli:[0,1,2],getreferencedsamplesid:2,querypar:2,content:[1,3,2],uploadfil:[0,2],getprojectbyus:[0,2],etag:2,print:0,model:[3,2],after:[0,2],insid:2,situat:2,plex:0,triggerobj:0,base:[0,1,2],dictionari:2,needsattent:2,org:1,"byte":2,md5:2,thread:2,doctre:1,musculu:0,filter:[0,2],place:[0,2],isn:2,nthese:0,first:0,rang:2,directli:0,onc:[0,2],propertylist:2,number:[0,2],yourself:0,restrict:2,alreadi:[0,2],done:0,triggertyp:0,miss:2,primari:0,getfiles3metadata:2,size:2,createbspath:2,given:2,script:0,data:[0,3,2],top:0,system:2,store:2,option:[0,2],urllib2:0,specifi:[0,2],getappsesss:2,github:0,accompani:0,staphylococcu:0,than:2,endpo:2,downloadfil:[0,2],instanc:[0,2],provid:[0,2],remov:1,tree:[0,3],project:[0,3,2],str:0,posit:[0,2],multipart:2,comput:0,ana:0,fastq:0,filtervariantset:2,argument:2,client_kei:0,myproject:0,packag:0,bacillu:0,manner:0,have:[0,1,2],tabl:3,need:[0,1,2],getfileurl:2,illegalparameterexcept:2,amplicon:0,startpo:2,getrunbyid:2,getintervalcoverag:[0,2],client:[0,2],note:[0,2],also:0,chromosom:[0,2],exampl:[0,2],take:0,indic:3,singl:2,sure:[0,1],modelnotinitializedexcept:2,test:[0,2],object:[0,2],getverificationcod:[0,2],fileurl:2,"class":[0,1,2],latex:1,getsampl:[0,2],url:2,doc:1,request:[0,3,2],obtainaccesstoken:2,v1pre2:0,part:[0,2],getappresultbyid:2,getrunpropertiesbyid:2,phix:0,getrunfilesbyid:2,show:0,text:[0,2],filetyp:2,session:0,saccharomyc:0,permiss:0,redirecturl:2,cereu:0,redirect:2,access:[0,3,2],onli:[0,2],locat:0,createappresult:2,illumina:0,multivaluepropertyappresultslist:2,getappsess:2,transact:0,solut:0,state:[0,2],haven:0,dict:2,analyz:0,folder:[0,2],analys:0,get:[0,1,2,3],familiar:0,stop:2,getsamplebyid:2,chrom:2,gen:0,requir:[0,2],accesstyp:2,enabl:0,undefinedparameterexcept:2,acut:2,remot:2,allgenom:0,gran:0,privileg:0,grab:0,bam:[0,3,2],basespaceapi:[0,2],summari:[0,2],set:2,see:[0,1],result:[0,3,2],respons:2,fail:2,subject:0,statu:[0,2],getappsessionpropertybynam:2,getaccessiblerunsbyus:2,appresult:[3,2],favor:2,written:1,between:0,"import":0,attribut:2,altern:0,kei:[0,2],buckets:0,filtervari:[0,2],popul:2,verification_with_code_uri:0,last:0,cov:[0,2],samplecount:0,region:2,equal:2,createproject:2,etc:2,redirect_uri:2,pdf:1,com:0,getcoveragemeta:[0,2],nsome:0,simpli:0,coveragemetadata:2,can:[0,2],instanti:0,clientsecret:2,applicationact:0,header:2,empti:2,suppli:0,getappresultpropertiesbyid:2,assum:0,devic:[0,2],due:2,been:[0,2],getaccessstr:[0,2],secret:[0,2],trigger:[0,3],rhodobact:0,interest:2,basic:0,addit:0,clientkei:2,getprojectpropertiesbyid:2,getsamplesbyproject:2,getcoveragemetainfo:2,coordin:2,cerevisia:0,repres:2,those:2,"case":2,multi:[0,2],look:0,access_token:0,plain:[0,2],align:2,properti:2,basespac:[0,2],coveragemeta:0,"while":2,na18507:0,myvcf:0,error:2,getappresultfil:2,setappsessionst:2,howev:2,loop:0,getavailablegenom:[0,2],file:[0,3,2],site:0,getappresult:2,myapi:0,basespacetestfil:0,s_g1_l001_r2_002:0,descript:[0,2],shutil:0,multipartfileupload:2,updateprivileg:2,par:2,disabl:2,develop:[0,1],sphaeroid:0,grant:[0,2],getapptrigg:0,make:[0,1,2],belong:0,createbsdir:2,same:[0,2],setstatu:0,handl:0,mkallberg:0,html:1,"2x151":0,document:[0,1],latexpdf:1,complet:[0,2],byterang:2,http:[0,1],resequenc:0,rais:2,temporari:2,user:[0,2],client_secret:0,appsess:[3,2],chang:[0,2],expand:0,bsauth:0,well:0,without:2,command:0,thi:[0,1,2],everyth:0,identifi:0,paus:0,sample_3:0,getvariantmetadata:2,obtain:0,rest:[0,2],sample_2:0,sample_1:0,human:0,outlin:0,yet:2,web:[0,2],cut:0,easi:0,getanalys:0,point:0,except:2,device_cod:0,add:[0,1],other:2,input:2,modul:[1,3,2],match:2,applic:[0,3,2],appresultfileupload:2,which:2,format:2,read:[0,2],piec:0,getvariantmeta:[0,2],isvalidfileopt:2,getappsessionpropertiesbyid:2,desc:2,applicationactionid:0,specif:[0,2],success:2,filenam:2,should:2,server:[0,2],href:0,necessari:0,either:[0,2],localdir:2,output:0,page:3,interv:[0,2],some:0,localpath:2,intern:2,homo:0,sampl:[0,3,2],analysisfil:0,aureu:0,octet:2,achiev:0,per:2,ucsc:0,larg:2,basespacepi:[0,1,2,3],approv:[0,2],getsamplefilesbyid:2,who:0,run:[0,1,2,3],ntype:0,nmy:0,step:0,apiserv:[0,2],bolt:0,src:0,about:[0,2],obj:2,getuserbyid:[0,2],genomev1:2,hg19:0,getaccess:2,produc:0,own:0,analysis2:0,within:[0,2],automat:0,mydir:0,s_g1_l001_r1_001:0,s_g1_l001_r1_002:0,your:0,getbasespaceapi:0,git:0,byterangeexcept:2,transfer:2,support:2,json:2,custom:2,avail:[0,1,2,3],start:[0,3,2],covmeta:0,includ:[0,2],"var":0,modelnotsupportedexcept:2,individu:2,analysi:[0,3],properli:[1,2],form:2,escherichia:0,projectlist:2,yourproject:0,link:1,oauth:0,"true":[0,2],sdk:[0,1],info:0,made:[0,2],arabidopsi:0,temp:2,possibl:2,"default":2,wish:[0,1,2],maximum:2,record:2,below:0,statussummari:2,processcount:2,"export":0,getappresultsbyproject:2,basespaceurl:0,displaynam:0,creat:[0,1,2,3],flow:0,uri:[0,2],exist:[1,2],kallberg:0,ing:2,check:0,fill:0,again:0,titl:1,when:[0,2],detail:0,invalid:2,field:2,valid:[0,2],tempdir:2,you:[0,1,2],nthe:0,nproject:0,ecoli:0,clean:1,sequenc:[0,2],nafter:0,docstr:1,ngenom:0,log:0,getreferencedsampl:2,sphinx:1,faster:0,directori:[0,1,2],deviceinfo:0,ignor:2,createanalysi:0,time:0,queryparamet:[3,2],profil:2},objtypes:{"0":"py:method","1":"py:class"},titles:["Getting Started","<no title>","Available modules","BaseSpacePy"],objnames:{"0":["py","method","Python method"],"1":["py","class","Python class"]},filenames:["Getting Started","README","Available modules","index"]})
\ No newline at end of file
diff --git a/doc/latex/BaseSpacePy.tex b/doc/latex/BaseSpacePy.tex
index 05ae4ff..b230124 100644
--- a/doc/latex/BaseSpacePy.tex
+++ b/doc/latex/BaseSpacePy.tex
@@ -1894,7 +1894,7 @@ \subsection{Availability}
 
 \subsection{Setup}
 \label{Getting Started:setup}
-\emph{Requirements:} Python 2.6 with the packages `urllib2', `pycurl', `multiprocessing' and `shutil' available.
+\emph{Requirements:} Python 2.6 with the packages `urllib2', `multiprocessing' and `shutil' available.
 
 The multi-part file upload will currently only run on a unix setup.
 
diff --git a/src/BaseSpacePy/api/APIClient.py b/src/BaseSpacePy/api/APIClient.py
index d068338..d25eea6 100644
--- a/src/BaseSpacePy/api/APIClient.py
+++ b/src/BaseSpacePy/api/APIClient.py
@@ -31,7 +31,7 @@ def __init__(self, AccessToken, apiServerAndVersion, userAgent=None, timeout=10)
 
     def __forcePostCall__(self, resourcePath, postData, headers):
         '''
-        For forcing a REST POST request using pycurl (seems to be used when POSTing with no post data)
+        For forcing a REST POST request (seems to be used when POSTing with no post data)
                 
         :param resourcePath: the url to call, including server address and api version
         :param postData: a dictionary of data to post
@@ -39,26 +39,19 @@ def __forcePostCall__(self, resourcePath, postData, headers):
         :returns: server response (a string containing json)
         '''
         import requests
-        # pycurl is hard to get working, so best to cauterise it into only the functions where it is needed
-        # import pycurl
-        # postData = [(p,postData[p]) for p in postData]
-        # headerPrep  = [k + ':' + headers[k] for k in headers.keys()]
-        # response = cStringIO.StringIO()
-        # c = pycurl.Curl()
-        # c.setopt(pycurl.URL,resourcePath + '?' + post)
-        # c.setopt(pycurl.HTTPHEADER, headerPrep)
-        # c.setopt(pycurl.POST, 1)
-        # c.setopt(pycurl.POSTFIELDS, post)
-        # c.setopt(c.WRITEFUNCTION, response.write)
-        # c.perform()
-        # c.close()
-        # return response.getvalue()
+        # this tries to clean up the output at the expense of letting the user know they're in an insecure context...
+        try:
+            requests.packages.urllib3.disable_warnings()
+        except:
+            pass
+        import logging
+        logging.getLogger("requests").setLevel(logging.WARNING)
         encodedPost =  urllib.urlencode(postData)
         resourcePath = "%s?%s" % (resourcePath, encodedPost)
         response = requests.post(resourcePath, data=json.dumps(postData), headers=headers)
         return response.text
 
-    def __putCall__(self, resourcePath, headers, transFile):
+    def __putCall__(self, resourcePath, headers, data):
         '''
         Performs a REST PUT call to the API server.
         
@@ -67,10 +60,21 @@ def __putCall__(self, resourcePath, headers, transFile):
         :param transFile: the name of the file containing only data to be PUT
         :returns: server response (a string containing upload status message (from curl?) followed by json response)
         '''
-        headerPrep  = [k + ':' + headers[k] for k in headers.keys()]        
-        cmd = 'curl -H "x-access-token:' + self.apiKey + '" -H "Content-MD5:' + headers['Content-MD5'].strip() +'" -T "'+ transFile +'" -X PUT ' + resourcePath
-        p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
-        return p.stdout.read()
+        # headerPrep  = [k + ':' + headers[k] for k in headers.keys()]
+        # cmd = 'curl -H "x-access-token:' + self.apiKey + '" -H "Content-MD5:' + headers['Content-MD5'].strip() +'" -T "'+ transFile +'" -X PUT ' + resourcePath
+        # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
+        # output = p.stdout.read()
+        # print output
+        # return output
+        import requests
+        put_headers = {
+            'Content-MD5'   :   headers['Content-MD5'].strip(),
+            'x-access-token':   self.apiKey
+        }
+        put_val = requests.put(resourcePath, data, headers=put_headers)
+        if put_val.status_code != 200:
+            raise ServerResponseException("Multi-part upload: Server return code %s with error %s" % (put_val.status_code, put_val.reason))
+        return put_val.text
 
     def callAPI(self, resourcePath, method, queryParams, postData, headerParams=None, forcePost=False):
         '''
@@ -132,13 +136,13 @@ def callAPI(self, resourcePath, method, queryParams, postData, headerParams=None
                 if data and not len(data): 
                     data='\n' # temp fix, in case is no data in the file, to prevent post request from failing
                 request = urllib2.Request(url=url, headers=headers, data=data)#,timeout=self.timeout)
-            else:                                    # use pycurl to force a post call, even w/o data
+            else:
                 response = self.__forcePostCall__(forcePostUrl, sentQueryParams, headers)
-            if method in ['PUT', 'DELETE']: #urllib doesnt do put and delete, default to pycurl here
+            if method in ['PUT', 'DELETE']:
                 if method == 'DELETE':
                     raise NotImplementedError("DELETE REST API calls aren't currently supported")
                 response = self.__putCall__(url, headers, data)
-                response =  response.split()[-1] # discard upload status msg (from curl put?)                
+                response =  response.split()[-1] # discard upload status msg (from curl put?)
         else:
             raise RestMethodException('Method ' + method + ' is not recognized.')
 
diff --git a/src/BaseSpacePy/api/AppLaunchHelpers.py b/src/BaseSpacePy/api/AppLaunchHelpers.py
index 2db5944..33fb1ef 100644
--- a/src/BaseSpacePy/api/AppLaunchHelpers.py
+++ b/src/BaseSpacePy/api/AppLaunchHelpers.py
@@ -19,22 +19,13 @@
 import os
 
 from BaseMountInterface import BaseMountInterface
-from BaseSpaceAPI import BaseSpaceAPI
-
-api = BaseSpaceAPI()
-API_VERSION = api.version
-
-SKIP_PROPERTIES = ["app-session-name"]
+from BaseSpaceException import ServerResponseException
 
 # if these strings are in the property names, we should not try to capture default values for them.
+# these are "global" but are needed by more than one object, so it's the cleanest way for now
 BS_ENTITIES = ["sample", "project", "appresult", "file"]
 BS_ENTITY_LIST_NAMES = ["Samples", "Projects", "AppResults", "Files"]
 
-LAUNCH_HEADER = {
-    "StatusSummary": "AutoLaunch",
-    "AutoStart": True,
-}
-
 
 class AppSessionMetaData(object):
     """
@@ -47,6 +38,8 @@ class AppSessionMetaData(object):
 
     __metaclass__ = abc.ABCMeta
 
+    SKIP_PROPERTIES = ["app-session-name", "attributes", "columns", "num-columns", "rowcount", "IsMultiNode"]
+
     def __init__(self, appsession_metadata):
         """
 
@@ -54,7 +47,58 @@ def __init__(self, appsession_metadata):
         """
         self.asm = appsession_metadata
 
+    def _get_all_duplicate_names(self):
+        appsession_properties = self.get_properties()
+        all_names = set()
+        duplicate_names = set()
+        for as_property in appsession_properties:
+            property_name = str(self.unpack_bs_property(as_property, "Name"))
+            if not property_name.startswith("Input"):
+                continue
+            property_basename = property_name.split(".")[-1]
+            if property_basename in self.SKIP_PROPERTIES:
+                continue
+            if property_basename in BS_ENTITY_LIST_NAMES:
+                continue
+            if property_basename in all_names:
+                duplicate_names.add(property_basename)
+            else:
+                all_names.add(property_basename)
+        return duplicate_names
+
+    @staticmethod
+    def _get_map_underlying_types(content):
+        """
+        Takes the content present in an existing appsession map type
+        and converts this into the underlying columns with their name and type
+        :param content: the raw content of the appsession we are deriving from
+        :return: a list of generic columns with name and type
+        """
+        # this should return a list of strings for a single row of the raw table
+        # this only gets us the names - we need to look up types later :(
+        first_row = content[0]
+        columns = [".".join(column.split(".")[:-1]) for column in first_row.Values]
+        return columns
+
+    @staticmethod
+    def _get_owning_map(all_properties, property_name):
+        # look for the property name without its suffix, since for an underlying property this will be eg. "row1"
+        property_name = ".".join(property_name.split(".")[:-1])
+        for property_ in all_properties:
+            if property_["Type"] == "map":
+                if property_name in property_["ColumnTypes"]:
+                    return property_
+        # if we didn't find a map with this property, return None
+        return None
+
     def get_refined_appsession_properties(self):
+        """
+        Unpacks the properties from an appsession and refines them ready to make a launch specification
+
+        :return: properties (list of dict of "Name" and "Type")
+                 defaults (dict from property name to default value)
+        """
+        all_names = self._get_all_duplicate_names()
         appsession_properties = self.get_properties()
         properties = []
         defaults = {}
@@ -63,10 +107,10 @@ def get_refined_appsession_properties(self):
             property_type = str(self.unpack_bs_property(as_property, "Type"))
             if not property_name.startswith("Input"):
                 continue
-            if property_name.count(".") != 1:
-                continue
             property_basename = property_name.split(".")[-1]
-            if property_basename in SKIP_PROPERTIES:
+            if property_basename in all_names:
+                property_basename = ".".join(property_name.split(".")[-2:])
+            if property_basename in self.SKIP_PROPERTIES:
                 continue
             if property_basename in BS_ENTITY_LIST_NAMES:
                 continue
@@ -74,6 +118,28 @@ def get_refined_appsession_properties(self):
                 "Name": property_name,
                 "Type": property_type,
             }
+            # this sets up the map, but we need to see examples of the columns in other properties to get the types
+            if property_type == "map":
+                content = self.unpack_bs_property(as_property, "Content")
+                columns = self._get_map_underlying_types(content)
+                this_property["ColumnNames"] = columns
+                # set a dict with an empty target that will be filled in later
+                this_property["ColumnTypes"] = dict((column, None) for column in columns)
+                properties.append(this_property)
+                continue
+            # check to see whether this property is part of an existing map property
+            owning_map = self._get_owning_map(properties, property_name)
+            if owning_map:
+                # if this property is part of a map, the last part of the name will be its row
+                # eg. Input.sample-experiments.happy-id.row1
+                # we should remove that suffix
+                property_name = ".".join(property_name.split(".")[:-1])
+                if owning_map["ColumnTypes"][property_name]:
+                    assert owning_map["ColumnTypes"][property_name] == property_type
+                else:
+                    owning_map["ColumnTypes"][property_name] = property_type
+                # if we are just using the property to grab types for a map, we shouldn't record it anywhere else
+                continue
             properties.append(this_property)
             bald_type = property_type.translate(None, "[]")
             if bald_type in BS_ENTITIES:
@@ -132,6 +198,9 @@ def get_app_name(self):
     def get_app_id(self):
         return self.asm.Application.Id
 
+    def get_app_version(self):
+        return self.asm.Application.VersionNumber
+
     @staticmethod
     def unpack_bs_property(bs_property, attribute):
         return getattr(bs_property, attribute)
@@ -147,6 +216,9 @@ def get_app_name(self):
     def get_app_id(self):
         return self.asm["Response"]["Application"]["Id"]
 
+    def get_app_version(self):
+        return self.asm["Response"]["Application"]["VersionNumber"]
+
     @staticmethod
     def unpack_bs_property(bs_property, attribute):
         return bs_property[attribute]
@@ -161,21 +233,84 @@ class LaunchSpecification(object):
     Class to help work with a BaseSpace app launch specification, which includes the properties and any defaults
     """
 
+    LAUNCH_HEADER = {
+        "StatusSummary": "AutoLaunch",
+        "AutoStart": True,
+    }
+
     def __init__(self, properties, defaults):
         self.properties = properties
+        self.cleaned_names = {}
         self.property_lookup = dict((self.clean_name(property_["Name"]), property_) for property_ in self.properties)
         self.defaults = defaults
 
-    @staticmethod
-    def clean_name(parameter_name):
+    def get_map_underlying_names_by_type(self, map_name, underlying_type):
+        map_property = self.property_lookup[map_name]
+        return [varname for varname, vartype in map_property["ColumnTypes"].iteritems() if vartype == underlying_type]
+
+    def get_map_position_by_underlying_name(self, map_name, underlying_name):
+        map_property = self.property_lookup[map_name]
+        return map_property["ColumnNames"].index(underlying_name)
+
+    def get_map_underlying_name_and_type_by_position(self, map_name, position):
+        map_property = self.property_lookup[map_name]
+        underlying_name = map_property["ColumnNames"][position]
+        underlying_type = map_property["ColumnTypes"][underlying_name]
+        return underlying_name, underlying_type
+
+    def get_map_underlying_types(self, map_name):
+        map_property = self.property_lookup[map_name]
+        return map_property["ColumnTypes"].values()
+
+    def clean_name(self, parameter_name):
         """
         strip off the Input. prefix, which is needed by the launch payload but gets in the way otherwise
         :param parameter_name: parameter name to clean
         :return: cleaned name
         """
-        prefix, cleaned_name = parameter_name.split(".")
-        assert prefix == "Input"
-        return cleaned_name
+        if not self.cleaned_names:
+            dup_names = set()
+            all_names = set()
+            for property_ in self.properties:
+                split_name = property_["Name"].split(".")
+                name_prefix = split_name[0]
+                assert name_prefix == "Input"
+                name_suffix = split_name[-1]
+                if name_suffix in all_names:
+                    dup_names.add(name_suffix)
+                else:
+                    all_names.add(name_suffix)
+            for property_ in self.properties:
+                full_name = property_["Name"]
+                split_name = full_name.split(".")
+                name_suffix = split_name[-1]
+                if name_suffix in dup_names:
+                    clean_name = ".".join(split_name[-2:])
+                else:
+                    clean_name = split_name[-1]
+                self.cleaned_names[full_name] = clean_name
+        return self.cleaned_names[parameter_name]
+
+    def process_parameter(self, param, varname):
+        # if option is prefixed with an @, it's a file (or process substitution with <() )
+        # so we should read inputs from there
+        # for properties with type map this is probably going to be prone to error :(
+        property_type = self.get_property_bald_type(varname)
+        if param.startswith("@") and property_type != "string":
+            assert self.is_list_property(varname), "cannot specify non-list parameter with file"
+            with open(param[1:]) as fh:
+                if property_type == "map":
+                    processed_param = [line.strip().split(",") for line in fh]
+                else:
+                    processed_param = [line.strip() for line in fh]
+        else:
+            if self.is_list_property(varname):
+                processed_param = param.split(",")
+            elif property_type == "map":
+                processed_param = [row.split(",") for row in param.split("::")]
+            else:
+                processed_param = param
+        return processed_param
 
     def resolve_list_variables(self, var_dict):
         """
@@ -189,7 +324,8 @@ def resolve_list_variables(self, var_dict):
             if self.is_list_property(varname) and not isinstance(varval, list):
                 var_dict[varname] = [varval]
                 # raise AppLaunchException("non-list property specified for list parameter")
-            if not self.is_list_property(varname) and isinstance(varval, list):
+            # if they've supplied a list, it must be for a list or map property
+            if (not self.is_list_property(varname) and self.get_property_type(varname) != "map") and isinstance(varval, list):
                 raise LaunchSpecificationException("list property specified for non-list parameter")
 
     @staticmethod
@@ -240,7 +376,7 @@ def make_sample_attribute_entry(sampleid, wrapped_sampleid, sample_attributes):
             this_sample_attributes.append(attribute_entry)
         return this_sample_attributes
 
-    def populate_properties(self, var_dict, sample_attributes={}):
+    def populate_properties(self, var_dict, api_version, sample_attributes={}):
         """
         Uses the base properties of the object and an instantiation of those properties (var_dict)
         build a dictionary that represents the launch payload
@@ -258,7 +394,7 @@ def populate_properties(self, var_dict, sample_attributes={}):
             all_sample_attributes = {
                 "Type": "map[]",
                 "Name": "Input.sample-id.attributes",
-                "items": []
+                "itemsmap": []
             }
         for property_ in populated_properties:
             property_name = self.clean_name(property_["Name"])
@@ -266,30 +402,56 @@ def populate_properties(self, var_dict, sample_attributes={}):
             bald_type = str(property_type).translate(None, "[]")
             property_value = var_dict[property_name]
             processed_value = ""
+            map_properties = []
             if bald_type in BS_ENTITIES:
                 if "[]" in property_type:
                     processed_value = []
                     for one_val in property_value:
-                        wrapped_value = "%s/%ss/%s" % (API_VERSION, bald_type, one_val)
+                        wrapped_value = "%s/%ss/%s" % (api_version, bald_type, one_val)
                         processed_value.append(wrapped_value)
                         if sample_attributes and bald_type == "sample":
                             one_sample_attributes = self.make_sample_attribute_entry(one_val, wrapped_value,
                                                                                      sample_attributes)
-                            all_sample_attributes["items"].append(one_sample_attributes)
+                            all_sample_attributes["itemsmap"].append(one_sample_attributes)
                 else:
-                    processed_value = "%s/%ss/%s" % (API_VERSION, bald_type, property_value)
+                    processed_value = "%s/%ss/%s" % (api_version, bald_type, property_value)
                     if sample_attributes and bald_type == "sample":
                         one_sample_attributes = self.make_sample_attribute_entry(property_value, processed_value,
                                                                                  sample_attributes)
                         sample_attributes["Items"].append(one_sample_attributes)
+            if bald_type == "map":
+                # for each argument, create one entry in the property list for each column
+                for rownum, row in enumerate(property_value):
+                    for colnum, value in enumerate(row):
+                        underlying_name, underlying_type = self.get_map_underlying_name_and_type_by_position(property_name, colnum)
+                        if underlying_type in BS_ENTITIES:
+                            wrapped_value = "%s/%ss/%s" % (api_version, underlying_type, value)
+                        else:
+                            wrapped_value = value
+                        assembled_args = {
+                            "Type" : underlying_type,
+                            "Name" : "%s.row%d" % (underlying_name, rownum+1),
+                            "Content" : wrapped_value
+                        }
+                        map_properties.append(assembled_args)
+                rowcount_entry = {
+                    "Type" : "string",
+                    "Name" : "%s.rowcount" % (property_name),
+                    "Content" : len(property_value)
+                }
+                map_properties.append(rowcount_entry)
+                # also create an entry for the number of columns
             if not processed_value:
                 processed_value = property_value
             if "[]" in property_type:
                 property_["items"] = processed_value
             else:
                 property_["Content"] = processed_value
+        populated_properties.extend(map_properties)
         if sample_attributes:
             populated_properties.append(all_sample_attributes)
+        # remove map properties, which aren't needed for launch
+        populated_properties = [property_ for property_ in populated_properties if property_["Type"] != "map"]
         return populated_properties
 
     def get_variable_requirements(self):
@@ -328,6 +490,11 @@ def get_property_bald_type(self, property_name):
         """
         return str(self.get_property_type(property_name)).translate(None, "[]")
 
+    def get_underlying_map_type(self, map_property_name, position):
+        map_property = self.property_lookup[map_property_name]
+        underlying_property_name = map_property["ColumnNames"][position]
+        return map_property["ColumnTypes"][underlying_property_name]
+
     def is_list_property(self, property_name):
         """
         is a given property a list property
@@ -343,7 +510,7 @@ def count_list_properties(self):
         """
         return [self.is_list_property(property_name) for property_name in self.get_minimum_requirements()].count(True)
 
-    def make_launch_json(self, user_supplied_vars, launch_name, sample_attributes={}, agent_id=""):
+    def make_launch_json(self, user_supplied_vars, launch_name, api_version, sample_attributes={}, agent_id=""):
         """
         build the launch payload (a json blob as a string) based on the supplied mapping from property name to value
 
@@ -355,7 +522,7 @@ def make_launch_json(self, user_supplied_vars, launch_name, sample_attributes={}
         :param agent_id: an AgentId to be attached to the launch, if specifed
         """
         # build basic headers
-        launch_dict = copy.copy(LAUNCH_HEADER)
+        launch_dict = copy.copy(self.LAUNCH_HEADER)
         launch_dict["Name"] = launch_name
         if agent_id:
             launch_dict["AgentId"] = agent_id
@@ -364,18 +531,17 @@ def make_launch_json(self, user_supplied_vars, launch_name, sample_attributes={}
         if required_vars - supplied_var_names:
             raise LaunchSpecificationException(
                 "Compulsory variable(s) missing! (%s)" % str(required_vars - supplied_var_names))
-        if supplied_var_names - (self.get_variable_requirements() | {"LaunchName"}):
+        if supplied_var_names - self.get_variable_requirements():
             print "warning! unused variable(s) specified: (%s)" % str(
                 supplied_var_names - self.get_variable_requirements())
         all_vars = copy.copy(self.defaults)
         all_vars.update(user_supplied_vars)
         self.resolve_list_variables(all_vars)
-        properties_dict = self.populate_properties(all_vars, sample_attributes)
+        properties_dict = self.populate_properties(all_vars, api_version, sample_attributes)
         launch_dict["Properties"] = properties_dict
         return json.dumps(launch_dict)
 
-    def format_property_information(self):
-        lines = ["\t".join(["Name", "Type", "Default"])]
+    def property_information_generator(self):
         minimum_requirements = self.get_minimum_requirements()
         for property_ in sorted(self.properties):
             property_name = self.clean_name(property_["Name"])
@@ -385,8 +551,14 @@ def format_property_information(self):
             output = [property_name, property_type]
             if property_name in self.defaults:
                 output.append(str(self.defaults[property_name]))
-            lines.append("\t".join(output))
-        return "\n".join(lines)
+            yield output
+
+    def format_property_information(self):
+        header = ["\t".join(["Name", "Type", "Default"])]
+        return "\n".join(header + ["\t".join(line) for line in self.property_information_generator()])
+
+    def format_map_types(self, property_name):
+        return ",".join(self.get_map_underlying_types(property_name))
 
     def dump_property_information(self):
         """
@@ -397,8 +569,16 @@ def dump_property_information(self):
 
     def format_minimum_requirements(self):
         minimum_requirements = self.get_minimum_requirements()
-        description = ["%s (%s)" % (varname, self.get_property_type(varname)) for varname in minimum_requirements]
-        return " ".join(description)
+        descriptions = []
+        for varname in minimum_requirements:
+            property_type = self.get_property_type(varname)
+            if property_type == "map":
+                description = "%s (%s[%s])" % (varname, property_type, self.format_map_types(varname))
+            else:
+                description = "%s (%s)" % (varname, property_type)
+            descriptions.append(description)
+        return " ".join(descriptions)
+
 
 class LaunchPayload(object):
     """
@@ -409,24 +589,73 @@ class LaunchPayload(object):
     and mapping BaseMount paths to the API reference strings the launch needs
     """
 
-    LAUNCH_NAME = "LaunchName"
+    ENTITY_TYPE_TO_METHOD_NAME = {
+        "sample": "getSampleById",
+        "appresult": "getAppResultById",
+        "project": "getProjectById"
+    }
 
-    def __init__(self, launch_spec, args, configoptions):
+    def __init__(self, launch_spec, args, configoptions, api, disable_consistency_checking=True):
         """
         :param launch_spec (LaunchSpecification)
         :param args (list) list or arguments to the app launch. These could be BaseSpace IDs or BaseMount paths
         :param configoptions (dict) key->value mapping for additional option values, such as genome-id
-        the ordering of args has to match the ordering of the sorted minimum requirements
+        :param api (BaseSpaceAPI) BaseSpace API object
+        :param disable_consistency_checking (bool) default behaviour is to ensure all entities and the launch itself
+            is done with the same access token. This parameter allows inconsistency (at user's risk!)
+        the ordering of args has to match the ordering of the sorted minimum requirements from the launch_spec
         It might be better to use a dict, but ultimately call order has to match at some point
         """
         self._launch_spec = launch_spec
         self._args = args
         self._configoptions = configoptions
+        self._api = api
+        self._access_token = None if disable_consistency_checking else api.apiClient.apiKey
+        varnames = self._launch_spec.get_minimum_requirements()
+        if len(varnames) != len(self._args):
+            raise LaunchSpecificationException("Number of arguments does not match specification")
+
+    def _arg_entry_to_name(self, entry, entity_type):
+        # if the argument contains a path separator, it must be a valid BaseMount path
+        # otherwise, an exception will be raised by BaseMountInterface
+        if os.path.sep in entry:
+            bmi = BaseMountInterface(entry)
+            return bmi.name
+        # if this is not a BaseMount path, try to resolve an entity name using the API
+        # note we're relying on the regular naming of the API to provide the right method name
+        # if this throws an exception, it's up to the caller to catch and handle
+        entry = entry.strip('"')
+        method = getattr(self._api, self.ENTITY_TYPE_TO_METHOD_NAME[entity_type])
+        return method(entry).Name
 
     def _find_all_entity_names(self, entity_type):
         """
         get all the entity names for a particular entity type
         used to make useful launch names
+
+        doing this for map types is pretty complicated. Imagine a map type defined like this:
+
+        {
+        "Name": "Input.sample-experiments",
+        "Type": "map",
+        "ColumnTypes": {
+          "sample-experiments.happy-id": "appresult",
+          "sample-experiments.result-label": "string"
+        },
+        "ColumnNames": [
+          "sample-experiments.happy-id",
+          "sample-experiments.result-label"
+        ]
+        }
+
+        and a map call like this:
+
+        [ [ "124124", "first" ] ,  [ "124127", "second" ] ]
+
+        then we want to look up the entity names of "124124" and "124127" based on their types
+        we get the type by looking up the variable in the position where the map call has been made
+        and then using that type definition to see where we can find the appropriate IDs in the underlying map
+
         :param entity_type: the entity type to look for
         :return: list of entity names
         """
@@ -434,13 +663,34 @@ def _find_all_entity_names(self, entity_type):
         varnames = self._launch_spec.get_minimum_requirements()
         for i, varname in enumerate(varnames):
             arg = self._args[i]
-            if self._launch_spec.get_property_bald_type(varname) == entity_type:
+            this_type = self._launch_spec.get_property_bald_type(varname)
+            if this_type == entity_type:
                 if not self._launch_spec.is_list_property(varname):
                     arg = [arg]
                 for entry in arg:
-                    if os.path.exists(entry):
-                        bmi = BaseMountInterface(entry)
-                        entity_names.append(bmi.name)
+                    try:
+                        name = self._arg_entry_to_name(entry, this_type)
+                        entity_names.append(name)
+                    # if we were unable to find a name, just press on
+                    except (AttributeError, ServerResponseException):
+                        pass
+            if this_type == "map":
+                # from the type we're after, get the variable name.
+                underlying_names = self._launch_spec.get_map_underlying_names_by_type(varname, entity_type)
+                # Use this to get the parameter positions.
+                varpositions = [self._launch_spec.get_map_position_by_underlying_name(varname, underlying_name)
+                                 for underlying_name in underlying_names]
+                # Use this to get the specifics for this call
+                for entry in arg:
+                    for position in varpositions:
+                        try:
+                            name = self._arg_entry_to_name(entry[position], entity_type)
+                            entity_names.append(name)
+                        # if we were unable to find a name, just press on
+                        except (AttributeError, ServerResponseException):
+                            pass
+
+
         return entity_names
 
     def derive_launch_name(self, app_name):
@@ -449,51 +699,63 @@ def derive_launch_name(self, app_name):
         :param app_name: name of app
         :return: useful name for app launch
         """
-        if self.LAUNCH_NAME in self._configoptions:
-            return self._configoptions[self.LAUNCH_NAME]
+        launch_names = self._find_all_entity_names("sample")
+        if not launch_names:
+            launch_names = self._find_all_entity_names("appresult")
+        if len(launch_names) > 3:
+            contracted_names = launch_names[:3] + ["%dmore" % (len(launch_names) - 3)]
+            launch_instance_name = "+".join(contracted_names)
         else:
-            launch_names = self._find_all_entity_names("sample")
-            if not launch_names:
-                launch_names = self._find_all_entity_names("appresult")
-            if len(launch_names) > 3:
-                contracted_names = launch_names[:3] + ["%dmore" % (len(launch_names) - 3)]
-                launch_instance_name = "+".join(contracted_names)
-            else:
-                launch_instance_name = "+".join(launch_names)
-            return "%s : %s" % (app_name, launch_instance_name)
+            launch_instance_name = "+".join(launch_names)
+        return "%s : %s" % (app_name, launch_instance_name)
 
     def is_valid_basespace_id(self, varname, basespace_id):
         """
         This is not really needed if users are specifying inputs as BaseMount paths,
         because in these cases validation happens elsewhere
-
-        To validate other kinds of ID, we should (TODO!) resolve the type based on the varname
-        and use the SDK to look it up.
         """
-        return True
+        vartype = self._launch_spec.get_property_bald_type(varname)
+        flat_vartype = vartype.lower()
+        if flat_vartype in self.ENTITY_TYPE_TO_METHOD_NAME:
+            method = getattr(self._api, self.ENTITY_TYPE_TO_METHOD_NAME[flat_vartype])
+            method(basespace_id)
+        else:
+            return True
 
-    def to_basespace_id(self, param_name, varval):
+    def preprocess_arg(self, param_type, varval):
         """
         Checks if a value for a parameter looks like a BaseMount path and tries to convert it into a BaseSpace ID
 
-        :param param_name: name of the parameter
+        :param param_type: type of the parameter
         :param varval: value of parameter
 
         :return basespaceid
         """
-        if varval.startswith("/") and not os.path.exists(varval):
-            raise LaunchSpecificationException("Parameter looks like a path, but does not exist: %s" % varval)
-        if os.path.exists(varval):
+        if param_type == "string":
+            return varval
+        if os.path.sep in varval:
+            # if the argument contains a path separator, it must be a valid BaseMount path
+            # otherwise, an exception will be raised by BaseMountInterface
             bmi = BaseMountInterface(varval)
-            spec_type = self._launch_spec.get_property_bald_type(param_name)
+            # make sure we have a BaseMount access token to compare - old versions won't have one
+            # also make sure we've been passed an access token -
+            # if we haven't, access token consistency checking has been disabled.
+            if bmi.access_token and self._access_token and bmi.access_token != self._access_token:
+                raise LaunchSpecificationException(
+                    "Access tokens between launch configuration and referenced BaseMount path do not match: %s" % varval)
             basemount_type = bmi.type
-            if spec_type != basemount_type:
+            if param_type != basemount_type:
                 raise LaunchSpecificationException(
-                    "wrong type of BaseMount path selected: %s needs to be of type %s" % (varval, spec_type))
+                    "wrong type of BaseMount path selected: %s needs to be of type %s" % (varval, param_type))
             bid = bmi.id
         else:
-            bid = varval
-        assert self.is_valid_basespace_id(param_name, bid)
+            # strip off quotes, which will be what comes in from bs list samples -f csv
+            bid = varval.strip('"')
+        # skip this step for now - it could be really expensive for big launches
+        # try:
+        #     self.is_valid_basespace_id(param_name, bid)
+        # except ServerResponseException as e:
+        #     raise LaunchSpecificationException("invalid BaseSpace ID '%s' for var: %s (%s)" % (varval, param_name, str(e)))
         return bid
 
     def get_args(self):
@@ -506,9 +768,26 @@ def get_args(self):
         for i, param_name in enumerate(params):
             arg = self._args[i]
             if isinstance(arg, list):
-                arg_map[param_name] = [self.to_basespace_id(param_name, arg_part) for arg_part in arg]
+                if isinstance(arg[0], list):
+                    preprocessed_rows = []
+                    # for each row
+                    for row in arg:
+                    # for each column
+                        preprocessed_row = []
+                        for position, column_value in enumerate(row):
+                            # look up type
+                            underlying_type = self._launch_spec.get_underlying_map_type(param_name, position)
+                            # convert
+                            preprocessed_arg = self.preprocess_arg(underlying_type, column_value)
+                            preprocessed_row.append(preprocessed_arg)
+                        preprocessed_rows.append(preprocessed_row)
+                    arg_map[param_name] = preprocessed_rows
+                else:
+                    param_type = self._launch_spec.get_property_bald_type(param_name)
+                    arg_map[param_name] = [self.preprocess_arg(param_type, arg_part) for arg_part in arg]
             else:
-                arg_map[param_name] = self.to_basespace_id(param_name, arg)
+                param_type = self._launch_spec.get_property_bald_type(param_name)
+                arg_map[param_name] = self.preprocess_arg(param_type, arg)
         return arg_map
 
     def get_all_variables(self):
diff --git a/src/BaseSpacePy/api/AuthenticationAPI.py b/src/BaseSpacePy/api/AuthenticationAPI.py
new file mode 100644
index 0000000..f0cd2ab
--- /dev/null
+++ b/src/BaseSpacePy/api/AuthenticationAPI.py
@@ -0,0 +1,161 @@
+import sys
+import time
+import ConfigParser
+import getpass
+import os
+import requests
+
+# this tries to clean up the output at the expense of letting the user know they're in an insecure context...
+try:
+    requests.packages.urllib3.disable_warnings()
+except:
+    pass
+import logging
+logging.getLogger("requests").setLevel(logging.WARNING)
+
+__author__ = 'psaffrey'
+
+"""
+Objects to help with creating config files that contain the right details to be used by the BaseSpaceSDK
+
+One way uses the OAuth flow for web application authentication:
+
+https://developer.basespace.illumina.com/docs/content/documentation/authentication/obtaining-access-tokens
+
+to get an access token and put it in the proper place.
+
+Also partly available here is obtaining session tokens (cookies), although these are not currently used.
+"""
+
+class AuthenticationException(Exception):
+    pass
+
+class AuthenticationScopeException(AuthenticationException):
+    pass
+
+class AuthenticationAPI(object):
+    DEFAULT_CONFIG_NAME = "DEFAULT"
+
+    def __init__(self, config_path, api_server):
+        self.config_path = config_path
+        self.api_server = api_server
+        self.config = None
+        self.parse_config()
+
+    def parse_config(self):
+        """
+        parses the config_path or creates it if it doesn't exist
+
+        :param config_path: path to config file
+        :return: ConfigParser object
+        """
+        self.config = ConfigParser.SafeConfigParser()
+        self.config.optionxform = str
+        if os.path.exists(self.config_path):
+            self.config.read(self.config_path)
+
+    def construct_default_config(self, api_server):
+        self.config.set(self.DEFAULT_CONFIG_NAME, "apiServer", api_server)
+
+    def write_config(self):
+        with open(self.config_path, "w") as fh:
+            self.config.write(fh)
+
+
+######
+# the BaseSpaceAPI doesn't support using the session tokens (cookies) at the moment
+# but this is here in case it's useful to somebody :)
+class SessionAuthentication(AuthenticationAPI):
+    SESSION_AUTH_URI = "https://accounts.illumina.com/"
+    SESSION_TOKEN_NAME = "sessionToken"
+    COOKIE_NAME = "IComLogin"
+
+    def basespace_session(self, username, password):
+        s = requests.session()
+        payload = {"UserName": username,
+                   "Password": password,
+                   "ReturnUrl": "http://developer.basespace.illumina.com/dashboard"}
+        r = s.post(url=self.SESSION_AUTH_URI,
+                   params={'Service': 'basespace'},
+                   data=payload,
+                   headers={'Content-Type': "application/x-www-form-urlencoded"},
+                   allow_redirects=False)
+        return s, r
+
+    def check_session_details(self):
+        pass
+
+    def set_session_details(self, config_path):
+        username = raw_input("username:")
+        password = getpass.getpass()
+        s, r = self.basespace_session(username, password)
+        self.config.set(self.DEFAULT_CONFIG_NAME, self.SESSION_TOKEN_NAME, r.cookies[self.COOKIE_NAME])
+        self.write_config()
+
+class OAuthAuthentication(AuthenticationAPI):
+    WAIT_TIME = 5.0
+    ACCESS_TOKEN_NAME = "accessToken"
+
+    def __init__(self, config_path, api_server, api_version):
+        super(OAuthAuthentication, self).__init__(config_path, api_server)
+        self.api_version = api_version
+
+    def set_oauth_details(self, client_id, client_secret, scopes):
+        scope_str = ",".join(scopes)
+        OAUTH_URI = "%s%s/oauthv2/deviceauthorization" % (self.api_server, self.api_version)
+        TOKEN_URI = "%s%s/oauthv2/token" % (self.api_server, self.api_version)
+        s = requests.session()
+        # make the initial request
+        auth_payload = {
+            "response_type": "device_code",
+            "client_id": client_id,
+            "scope": scope_str,
+        }
+        try:
+            r = s.post(url=OAUTH_URI,
+                       data=auth_payload)
+        except Exception as e:
+            raise AuthenticationException("Failed to communicate with server: %s" % str(e))
+        # show the URL to the user
+        try:
+            payload = r.json()
+        except ValueError:
+            raise AuthenticationException("bad payload from server - perhaps you should use https instead of http?")
+        if 'error' in payload:
+            if payload['error'] == 'invalid_scope':
+                raise AuthenticationScopeException("Authentication requested with invalid scope: %s" % scope_str)
+            else:
+                msg = payload['error_description'] if 'error_description' in payload else payload['error']
+                raise AuthenticationException(msg)
+        auth_url = payload["verification_with_code_uri"]
+        auth_code = payload["device_code"]
+        print "please authenticate here: %s" % auth_url
+        # poll the token URL until we get the token
+        token_payload = {
+            "client_id": client_id,
+            "client_secret": client_secret,
+            "code": auth_code,
+            "grant_type": "device"
+        }
+        access_token = None
+        while 1:
+            # put the token into the config file
+            r = s.post(url=TOKEN_URI,
+                       data=token_payload)
+            if r.status_code == 400:
+                if r.json()["error"] == "access_denied" or r.json()["error"] == "AccessDenied":
+                    sys.stdout.write("\n")
+                    break
+                sys.stdout.write(".")
+                sys.stdout.flush()
+                time.sleep(self.WAIT_TIME)
+            else:
+                sys.stdout.write("\n")
+                access_token = r.json()["access_token"]
+                break
+        self.construct_default_config(self.api_server)
+        if not access_token:
+            raise Exception("problem obtaining token!")
+        print "Success!"
+        self.config.set(self.DEFAULT_CONFIG_NAME, self.ACCESS_TOKEN_NAME, access_token)
+        self.write_config()
\ No newline at end of file
diff --git a/src/BaseSpacePy/api/BaseAPI.py b/src/BaseSpacePy/api/BaseAPI.py
index da2de39..69de48c 100644
--- a/src/BaseSpacePy/api/BaseAPI.py
+++ b/src/BaseSpacePy/api/BaseAPI.py
@@ -12,14 +12,14 @@
 from BaseSpacePy.api.APIClient import APIClient
 from BaseSpacePy.api.BaseSpaceException import *
 from BaseSpacePy.model import *
-
+from itertools import chain
 
 class BaseAPI(object):
     '''
     Parent class for BaseSpaceAPI and BillingAPI classes
     '''
 
-    def __init__(self, AccessToken, apiServerAndVersion, userAgent, timeout=10, verbose=False):
+    def __init__(self, AccessToken, apiServerAndVersion, userAgent=None, timeout=10, verbose=False):
         '''
         :param AccessToken: the current access token
         :param apiServerAndVersion: the api server URL with api version
@@ -51,7 +51,7 @@ def __singleRequest__(self, myModel, resourcePath, method, queryParams, headerPa
         :param headerParams: a dictionary of header parameters
         :param postData: (optional) data to POST, default None
         :param version: (optional) print detailed output, default False
-        :param forcePost: (optional) use a POST call with pycurl instead of urllib, default False (used only when POSTing with no post data?)
+        :param forcePost: (optional) use a POST call, default False (used only when POSTing with no post data?)
 
         :raises ServerResponseException: if server returns an error or has no response
         :returns: an instance of the Response model from the provided myModel
@@ -83,16 +83,20 @@ def __singleRequest__(self, myModel, resourcePath, method, queryParams, headerPa
         else:
             return responseObject
 
-    def __listRequest__(self, myModel, resourcePath, method, queryParams, headerParams):
+    def __listRequest__(self, myModel, resourcePath, method, queryParams, headerParams, sort=True):
         '''
         Call a REST API that returns a list and deserialize response into a list of objects of the provided model.
         Handles errors from server.
 
+        Sorting by date for each call is the default, so that if a new item is created while we're paging through
+        we'll pick it up at the end. However, this should be switched off for some calls (like variants)
+
         :param myModel: a Model type to return a list of
         :param resourcePath: the api url path to call (without server and version)
         :param method: the REST method type, eg. GET
         :param queryParams: a dictionary of query parameters
         :param headerParams: a dictionary of header parameters
+        :param sort: sort the outputs from the API to prevent race-conditions
 
         :raises ServerResponseException: if server returns an error or has no response        
         :returns: a list of instances of the provided model
@@ -103,47 +107,66 @@ def __listRequest__(self, myModel, resourcePath, method, queryParams, headerPara
             print '    # Path:      ' + str(resourcePath)
             print '    # QPars:     ' + str(queryParams)
             print '    # Hdrs:      ' + str(headerParams)
-        response = self.apiClient.callAPI(resourcePath, method, queryParams, None, headerParams)
-        if self.verbose:
-            self.__json_print__('    # Response:  ',response)
-        if not response: 
-            raise ServerResponseException('No response returned')
-        if response['ResponseStatus'].has_key('ErrorCode'):
-            raise ServerResponseException(str(response['ResponseStatus']['ErrorCode'] + ": " + response['ResponseStatus']['Message']))
-        elif response['ResponseStatus'].has_key('Message'):
-            raise ServerResponseException(str(response['ResponseStatus']['Message']))
-        
-        respObj = self.apiClient.deserialize(response, ListResponse.ListResponse)
-        return [self.apiClient.deserialize(c, myModel) for c in respObj._convertToObjectList()]
+        number_received = 0
+        total_number = None
+        responses = []
+        # if the user explicitly sets a Limit in queryParams, just make one call with that limit
+        justOne = False
+        if "Limit" in queryParams:
+            justOne = True
+        else:
+            queryParams["Limit"] = 1024
+        if sort:
+            queryParams["SortBy"] = "DateCreated"
+        while total_number is None or number_received < total_number:
+            queryParams["Offset"] = number_received
+            response = self.apiClient.callAPI(resourcePath, method, queryParams, None, headerParams)
+            if self.verbose:
+                self.__json_print__('    # Response:  ',response)
+            if not response:
+                raise ServerResponseException('No response returned')
+            if response['ResponseStatus'].has_key('ErrorCode'):
+                raise ServerResponseException(str(response['ResponseStatus']['ErrorCode'] + ": " + response['ResponseStatus']['Message']))
+            elif response['ResponseStatus'].has_key('Message'):
+                raise ServerResponseException(str(response['ResponseStatus']['Message']))
+
+            respObj = self.apiClient.deserialize(response, ListResponse.ListResponse)
+            responses.append(respObj)
+            if justOne:
+                break
+            # if a TotalCount is not an attribute, assume we have all of them (eg. variantsets)
+            if not hasattr(respObj.Response, "TotalCount"):
+                break
+            # allow the total number to change on each call
+            # to catch the race condition where a new entity appears while we're calling
+            total_number = respObj.Response.TotalCount
+            if total_number > 0 and respObj.Response.DisplayedCount == 0:
+                # sometimes the API DisplayedCount and TotalCount don't match :(
+                # if there are none left, just return what we've found already
+                break
+                #raise ServerResponseException("Paged query returned no results")
+            number_received += respObj.Response.DisplayedCount
+
+        return [self.apiClient.deserialize(c, myModel) for c in chain(*[ ro._convertToObjectList() for ro in responses ])]
 
     def __makeCurlRequest__(self, data, url):
         '''
         Make a curl POST request
-        
+
         :param data: data to post (eg. list of tuples of form (key, value))
         :param url: url to post data to
-        
+
         :raises ServerResponseException: if server returns an error or has no response
         :returns: dictionary of api server response
         '''
-        # pycurl is hard to get working, so best to cauterise it into only the functions where it is needed
-        import pycurl
-        post = urllib.urlencode(data)
-        response = cStringIO.StringIO()
-        c = pycurl.Curl()
-        c.setopt(pycurl.URL,url)
-        c.setopt(pycurl.POST, 1)
-        c.setopt(pycurl.POSTFIELDS, post)
-        c.setopt(c.WRITEFUNCTION, response.write)
-        c.perform()
-        c.close()
-        respVal = response.getvalue()
-        if not respVal:
+        import requests
+        r = requests.post(url, data)
+        if not r:
             raise ServerResponseException("No response from server")
-        obj = json.loads(respVal)
+        obj = json.loads(r.text)
         if obj.has_key('error'):
             raise ServerResponseException(str(obj['error'] + ": " + obj['error_description']))
-        return obj      
+        return obj
 
     def getTimeout(self):
         '''
diff --git a/src/BaseSpacePy/api/BaseMountInterface.py b/src/BaseSpacePy/api/BaseMountInterface.py
index 74c8fc2..199f240 100644
--- a/src/BaseSpacePy/api/BaseMountInterface.py
+++ b/src/BaseSpacePy/api/BaseMountInterface.py
@@ -1,7 +1,7 @@
 """
 class to wrap a directory mounted using BaseMount and provide convenience functions to get at the metadata stored there
 
-The currently supported metadata extraction uses the files created by metaBSFS, but is limited to first class entities like projects and samples
+The currently supported metadata extraction uses the files created by BaseMount, but is limited to first class entities like projects and samples
 it will fail (and throw an exception) when pointing at other directories. For some of these it's not really clear what the behaviour *should* be
 eg. ~/BaseSpace/current_user/Projects/Sloths\ Test/Samples/
 which is the owning directory for the "Sloths Test" samples. Should this be a directory of type "project" and id of the "Hyperion Test" project?
@@ -22,18 +22,21 @@ class BaseMountInterface(object):
     def __init__(self, path):
         if path.endswith(os.sep):
             path = path[:-1]
-        self.path = path
+        self.path = os.path.expanduser(path)
         self.id = None
         self.type = None
+        self.access_token = None
         self.name = os.path.basename(path)
         if not self.__validate_basemount__():
-            raise BaseMountInterfaceException("Path: %s does not seem to be a BaseMount path" % self.path)
+            raise BaseMountInterfaceException("Path: %s does not seem to be a BaseMount entity path" % self.path)
         self.__resolve_details__()
 
     def __validate_basemount__(self):
         """
-        Checks whether the chosen directory is a BSFS directory
+        Checks whether the chosen directory is a BaseMount directory
         """
+        if not os.path.exists(self.path):
+            return False
         if os.path.isdir(self.path):
             for required in REQUIRED_ENTRIES:
                 required_path = os.path.join(self.path, required)
@@ -43,7 +46,7 @@ def __validate_basemount__(self):
 
     def __resolve_details__(self):
         """
-        pull the useful details out of the . files generated by metaBSFS
+        pull the useful details out of the . files generated by BaseMount
         """
         if os.path.isdir(self.path):
             type_file = os.path.join(self.path, ".type")
@@ -58,13 +61,30 @@ def __resolve_details__(self):
         if self.type == "file":
             metadata_path = self.path.replace("Files", "Files.metadata")
             id_file = os.path.join(metadata_path, ".id")
+            config_file = os.path.join(os.path.dirname(self.path), ".basemount", "Config.cfg")
         else:
             id_file = os.path.join(self.path, ".id")
+            config_file = os.path.join(self.path, ".basemount", "Config.cfg")
+        if os.path.isfile(config_file):
+            # get the access token if we can
+            self.access_token = self._get_access_token_from_config(config_file)
         self.id = open(id_file).read().strip()
 
     def __str__(self):
         return "%s : (%s) : (%s)" % (self.path, self.id, self.type)
 
+    def _get_access_token_from_config(self, config_path):
+        from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError
+        config = SafeConfigParser()
+        config.read(config_path)
+        try:
+            return config.get("DEFAULT", "accessToken")
+        except NoOptionError, NoSectionError:
+            raise BaseMountInterfaceException("malformed BaseMount config: %s" % config_path)
+
+
+
+
     def get_meta_data(self):
         try:
             with open(os.path.join(self.path, ".json")) as fh:
diff --git a/src/BaseSpacePy/api/BaseSpaceAPI.py b/src/BaseSpacePy/api/BaseSpaceAPI.py
index ba8ccbe..de63c4c 100755
--- a/src/BaseSpacePy/api/BaseSpaceAPI.py
+++ b/src/BaseSpacePy/api/BaseSpaceAPI.py
@@ -13,6 +13,8 @@
 import ConfigParser
 import urlparse
 import logging
+import getpass
+import requests
 
 from BaseSpacePy.api.APIClient import APIClient
 from BaseSpacePy.api.BaseAPI import BaseAPI
@@ -37,7 +39,7 @@ class BaseSpaceAPI(BaseAPI):
     '''
     The main API class used for all communication with the REST server
     '''
-    def __init__(self, clientKey=None, clientSecret=None, apiServer=None, version=None, appSessionId='', AccessToken='', userAgent=None, timeout=10, verbose=0, profile='DEFAULT'):
+    def __init__(self, clientKey=None, clientSecret=None, apiServer=None, version='v1pre3', appSessionId='', AccessToken='', userAgent=None, timeout=10, verbose=0, profile='default'):
         '''
         The following arguments are required in either the constructor or a config file (~/.basespacepy.cfg):        
         
@@ -62,7 +64,7 @@ def __init__(self, clientKey=None, clientSecret=None, apiServer=None, version=No
             self.profile    = cred['profile']
         # TODO this replacement won't work for all environments
         self.weburl         = cred['apiServer'].replace('api.','')
-        
+
         apiServerAndVersion = urlparse.urljoin(cred['apiServer'], cred['apiVersion'])
         super(BaseSpaceAPI, self).__init__(cred['accessToken'], apiServerAndVersion, userAgent, timeout, verbose)
 
@@ -79,109 +81,84 @@ def _setCredentials(self, clientKey, clientSecret, apiServer, apiVersion, appSes
         :param version: the version of the BaseSpace API
         :param appSessionId: the AppSession Id
         :param AccessToken: an access token        
-        :param profile: name of profile in config file        
+        :param profile: name of the config file
         :returns: dictionary with credentials from constructor, config file, or default (for optional args), in this priority order.
         '''
         lcl_cred = self._getLocalCredentials(profile)
+        my_path = os.path.dirname(os.path.abspath(__file__))
+        authenticate_cmd = "bs authenticate"
+        if profile != "default":
+            authenticate_cmd = "%s --config %s" % (authenticate_cmd, profile)
         cred = {}
+        # if access tokens have not been provided through the constructor,
+        # set a profile name
+        if not accessToken:
+            if 'name' in lcl_cred:
+                cred['profile'] = lcl_cred['name']
+            else:
+                cred['profile'] = profile
         # required credentials
-        if clientKey is not None:
-            cred['clientKey'] = clientKey
-        else:
-            try:
-                cred['clientKey'] = lcl_cred['clientKey']
-            except KeyError:        
-                raise CredentialsException('Client Key not available - please provide in BaseSpaceAPI constructor or config file')
+        REQUIRED = ["accessToken", "apiServer", "apiVersion"]
+        for conf_item in REQUIRED:
+            local_value = locals()[conf_item]
+            if local_value:
+               cred[conf_item] = local_value
             else:
-                # set profile name
-                if 'name' in lcl_cred:
-                    cred['profile'] = lcl_cred['name']
-                else:
-                    cred['profile'] = profile
-        if clientSecret is not None:
-            cred['clientSecret'] = clientSecret
-        else:
-            try:
-                cred['clientSecret'] = lcl_cred['clientSecret']
-            except KeyError:        
-                raise CredentialsException('Client Secret not available - please provide in BaseSpaceAPI constructor or config file')
-        if apiServer is not None:
-            cred['apiServer'] = apiServer
-        else:
-            try:
-                cred['apiServer'] = lcl_cred['apiServer']
-            except KeyError:        
-                raise CredentialsException('API Server URL not available - please provide in BaseSpaceAPI constructor or config file')
-        if apiVersion is not None:
-            cred['apiVersion'] = apiVersion
-        else:
-            try:
-                cred['apiVersion'] = lcl_cred['apiVersion']
-            except KeyError:        
-                raise CredentialsException('API version available - please provide in BaseSpaceAPI constructor or config file')        
-        # Optional credentials 
-        if appSessionId:
-            cred['appSessionId'] = appSessionId
-        elif 'apiVersion' in lcl_cred:
-            try:
-                cred['appSessionId'] = lcl_cred['appSessionId']
-            except KeyError:
-                cred['appSessionId'] = appSessionId
-        else:
-            cred['appSessionId'] = appSessionId
-        
-        if accessToken:
-            cred['accessToken'] = accessToken
-        elif 'accessToken' in lcl_cred:            
-            try:
-                cred['accessToken'] = lcl_cred['accessToken']
-            except KeyError:
-                cred['accessToken'] = accessToken
-        else:
-            cred['accessToken'] = accessToken
-        
-        return cred            
+                try:
+                    cred[conf_item] = lcl_cred[conf_item]
+                except KeyError:
+                    raise CredentialsException("%s not found or config %s missing. Try running \"%s\"" % (conf_item, profile, authenticate_cmd))
+        # Optional credentials
+        OPTIONAL = ["clientKey", "clientSecret", "appSessionId"]
+        for conf_item in OPTIONAL:
+            local_value = locals()[conf_item]
+            if local_value:
+                cred[conf_item] = local_value
+            else:
+                try:
+                    cred[conf_item] = lcl_cred[conf_item]
+                except KeyError:
+                    cred[conf_item] = local_value
+        return cred
 
     def _getLocalCredentials(self, profile):
         '''
-        Returns credentials from local config file (~/.basespacepy.cfg)
+        Returns credentials from local config file (~/.basespace/.cfg)
         If some or all credentials are missing, they aren't included the in the returned dict
         
-        :param profile: Profile name from local config file 
+        :param profile: Profile name to use to find local config file
         :returns: A dictionary with credentials from local config file 
         '''
-        config_file = os.path.expanduser('~/.basespacepy.cfg')
+        config_file = os.path.join(os.path.expanduser('~/.basespace'), "%s.cfg" % profile)
+        if not os.path.exists(config_file):
+            raise CredentialsException("Could not find config file: %s" % config_file)
+        section_name = "DEFAULT"
         cred = {}        
         config = ConfigParser.SafeConfigParser()
         if config.read(config_file):
-            if not config.has_section(profile) and profile.lower() != 'default':                
-                raise CredentialsException("Profile name '%s' not present in config file %s" % (profile, config_file))
+            cred['name'] = profile
             try:
-                cred['name'] = config.get(profile, "name")
+                cred['clientKey'] = config.get(section_name, "clientKey")
             except ConfigParser.NoOptionError:
                 pass
             try:
-                cred['clientKey'] = config.get(profile, "clientKey")
+                cred['clientSecret'] = config.get(section_name, "clientSecret")
             except ConfigParser.NoOptionError:
                 pass
             try:
-                cred['clientSecret'] = config.get(profile, "clientSecret")
+                cred['apiServer'] = config.get(section_name, "apiServer")
             except ConfigParser.NoOptionError:
                 pass
             try:
-                cred['apiServer'] = config.get(profile, "apiServer")
-            except ConfigParser.NoOptionError:
-                pass
-            try:
-                cred['apiVersion'] = config.get(profile, "apiVersion")
+                cred['apiVersion'] = config.get(section_name, "apiVersion")
             except ConfigParser.NoOptionError:
                 pass
             try: 
-                cred['appSessionId'] = config.get(profile, "appSessionId")
+                cred['appSessionId'] = config.get(section_name, "appSessionId")
             except ConfigParser.NoOptionError:
                 pass
             try:
-                cred['accessToken'] = config.get(profile, "accessToken")
+                cred['accessToken'] = config.get(section_name, "accessToken")
             except ConfigParser.NoOptionError:
                 pass            
         return cred
@@ -204,7 +181,6 @@ def getAppSessionOld(self, Id=None):
         :param Id: an AppSession Id; if not provided, the AppSession Id of the BaseSpaceAPI instance will be used 
         :returns: An AppSession instance                
         '''
-        # pycurl is hard to get working, so best to cauterise it into only the functions where it is needed
         if Id is None:
             Id = self.appSessionId
         if not Id:
@@ -212,14 +188,6 @@ def getAppSessionOld(self, Id=None):
         resourcePath = self.apiClient.apiServerAndVersion + '/appsessions/{AppSessionId}'        
         resourcePath = resourcePath.replace('{AppSessionId}', Id)        
         response = cStringIO.StringIO()
-        # import pycurl
-        # c = pycurl.Curl()
-        # c.setopt(pycurl.URL, resourcePath)
-        # c.setopt(pycurl.USERPWD, self.key + ":" + self.secret)
-        # c.setopt(c.WRITEFUNCTION, response.write)
-        # c.perform()
-        # c.close()
-        # resp_dict = json.loads(response.getvalue())        
         import requests
         response = requests.get(resourcePath, auth=(self.key, self.secret))
         resp_dict = json.loads(response.text)
@@ -237,6 +205,13 @@ def getAppSession(self, Id=None, queryPars=None):
         queryParams = {}
         return self.__singleRequest__(AppSessionResponse.AppSessionResponse, resourcePath, method, queryParams, headerParams)
 
+    def getAllAppSessions(self, queryPars=None):
+        queryParams = self._validateQueryParameters(queryPars)
+        resourcePath = '/users/current/appsessions'
+        method = 'GET'
+        headerParams = {}
+        return self.__listRequest__(AppSession.AppSession, resourcePath, method, queryParams, headerParams)
+
     def __deserializeAppSessionResponse__(self, response):
         '''
         Converts a AppSession response from the API server to an AppSession object.        
@@ -322,6 +297,25 @@ def setAppSessionState(self, Id, Status, Summary):
         postData['statussummary'] = Summary
         return self.__singleRequest__(AppSessionResponse.AppSessionResponse, resourcePath, method, queryParams, headerParams, postData=postData)
 
+    def stopAppSession(self, Id):
+        """
+        Unfortunately, the v1pre3 appsession stop endpoint does not support tokens,
+        so this method has to create a special API object to call a v2 endpoint :(
+
+        :param Id:
+        :return: An AppSessionResponse that contains the appsession we just stopped
+        """
+        resourcePath = '/appsessions/{Id}/stop'
+        method = 'POST'
+        resourcePath = resourcePath.replace('{Id}', Id)
+        queryParams = {}
+        headerParams = {}
+        postData = {}
+        apiServerAndVersion = urlparse.urljoin(self.apiServer, "v2")
+        v2api = BaseAPI(self.getAccessToken(), apiServerAndVersion)
+        return v2api.__singleRequest__(AppSessionResponse.AppSessionResponse, resourcePath, method, queryParams,
+                                  headerParams, postData=postData)
+
     def __deserializeObject__(self, dct, type):
         '''
         Converts API response into object instances for Projects, Samples, and AppResults.
@@ -403,6 +397,35 @@ def obtainAccessToken(self, code, grantType='device', redirect_uri=None):
         resp_dict = self.__makeCurlRequest__(data, self.apiClient.apiServerAndVersion + tokenURL)
         return str(resp_dict['access_token'])
 
+    def getAccessTokenDetails(self):
+        '''
+        Because this does not use the standard API prefix, this has to be done as a special case
+        :return:
+        '''
+        endpoint = self.apiClient.apiServerAndVersion + tokenURL + "/current"
+
+        args = {
+            "access_token": self.apiClient.apiKey
+        }
+        try:
+            response_raw = requests.get(endpoint, args)
+            response = response_raw.json()
+        except Exception as e:
+            raise ServerResponseException('Could not query access token endpoint: %s' % str(e))
+        if not response:
+            raise ServerResponseException('No response returned')
+        if response.has_key('ResponseStatus'):
+            if response['ResponseStatus'].has_key('ErrorCode'):
+                raise ServerResponseException(str(response['ResponseStatus']['ErrorCode'] + ": " + response['ResponseStatus']['Message']))
+            elif response['ResponseStatus'].has_key('Message'):
+                raise ServerResponseException(str(response['ResponseStatus']['Message']))
+        elif response.has_key('ErrorCode'):
+            raise ServerResponseException(response["MessageFormatted"])
+
+        responseObject = self.apiClient.deserialize(response["Response"], Token.Token)
+        return responseObject
+
+
     def updatePrivileges(self, code, grantType='device', redirect_uri=None):
         '''
         Retrieves a user-specific access token, and sets the token on the current object.
@@ -605,7 +628,19 @@ def getProjectByUser(self, queryPars=None):
         method = 'GET'        
         headerParams = {}
         return self.__listRequest__(Project.Project,resourcePath, method, queryParams, headerParams)
-       
+
+    def getUserProjectByName(self, projectName):
+        '''
+
+        :return: project matching the provided name
+        '''
+        projects = self.getProjectByUser(qp({"Name":projectName}))
+        if len(projects) == 0:
+            raise ServerResponseException("No such project: %s" % projectName)
+        if len(projects) > 1:
+            raise ServerResponseException("More than one matching projects: %s" % projectName)
+        return projects[0]
+
     def getAccessibleRunsByUser(self, queryPars=None):
         '''
         Returns a list of accessible runs for the current User
@@ -703,15 +738,15 @@ def getAppResultsByProject(self, Id, queryPars=None, statuses=None):
     def getSamplesByProject(self, Id, queryPars=None):
         '''
         Returns a list of samples associated with a project with Id
-        
+
         :param Id: The id of the project
         :param queryPars: An (optional) object of type QueryParameters for custom sorting and filtering
         :returns: a list of Sample instances
         '''
-        queryParams = self._validateQueryParameters(queryPars)                
+        queryParams = self._validateQueryParameters(queryPars)
         resourcePath = '/projects/{Id}/samples'
         resourcePath = resourcePath.replace('{format}', 'json')
-        method = 'GET'        
+        method = 'GET'
         headerParams = {}
         resourcePath = resourcePath.replace('{Id}',Id)
         return self.__listRequest__(Sample.Sample,resourcePath, method, queryParams, headerParams)
@@ -836,7 +871,7 @@ def getAvailableGenomes(self, queryPars=None):
         method = 'GET'
         headerParams = {}
         return self.__listRequest__(GenomeV1.GenomeV1,
-                                    resourcePath, method, queryParams, headerParams)
+                                    resourcePath, method, queryParams, headerParams, sort=False)
 
     def getIntervalCoverage(self, Id, Chrom, StartPos, EndPos):
         '''
@@ -902,7 +937,7 @@ def filterVariantSet(self,Id, Chrom, StartPos, EndPos, Format='json', queryPars=
         if Format == 'vcf':
             raise NotImplementedError("Returning native VCF format isn't yet supported by BaseSpacePy")
         else:
-            return self.__listRequest__(Variant.Variant, resourcePath, method, queryParams, headerParams)
+            return self.__listRequest__(Variant.Variant, resourcePath, method, queryParams, headerParams, sort=False)
 
     def getVariantMetadata(self, Id, Format='json'):
         '''        
@@ -1152,7 +1187,7 @@ def __finalizeMultipartFileUpload__(self, Id):
         return self.__singleRequest__(FileResponse.FileResponse,
                                       resourcePath, method, queryParams, headerParams, postData=postData, forcePost=1)
 
-    def multipartFileUpload(self, resourceType, resourceId, localPath, fileName, directory, contentType, tempDir=None, processCount=10, partSize=25):
+    def multipartFileUpload(self, resourceType, resourceId, localPath, fileName, directory, contentType, processCount=10, partSize=25):
         '''
         Method for multi-threaded file-upload for parallel transfer of very large files (currently only runs on unix systems)
         
@@ -1162,7 +1197,6 @@ def multipartFileUpload(self, resourceType, resourceId, localPath, fileName, dir
         :param fileName: The desired filename on the server
         :param directory: The desired directory name on the server (empty string will place it in the root directory)
         :param contentType: The content type of the file
-        :param tempdir: (optional) Temp directory to use for temporary file chunks to upload
         :param processCount: (optional) The number of processes to be used, default 10
         :param partSize: (optional) The size in MB of individual upload parts (must be >5 Mb and <=25 Mb), default 25
         :returns: a File instance, which has been updated after the upload has completed.
@@ -1172,10 +1206,8 @@ def multipartFileUpload(self, resourceType, resourceId, localPath, fileName, dir
         # First create file object in BaseSpace, then create multipart upload object and start upload
         if partSize <= 5 or partSize > 25:
             raise UploadPartSizeException("Multipart upload partSize must be >5 MB and <=25 MB")
-        if tempDir is None:
-            tempDir = mkdtemp()
         bsFile = self.__initiateMultipartFileUpload__(resourceType, resourceId, fileName, directory, contentType)
-        myMpu = mpu(self, localPath, bsFile, processCount, partSize, temp_dir=tempDir)                
+        myMpu = mpu(self, localPath, bsFile, processCount, partSize)
         return myMpu.upload()                
 
     def multipartFileUploadSample(self, Id, localPath, fileName, directory, contentType, tempDir=None, processCount=10, partSize=25):
@@ -1196,7 +1228,10 @@ def multipartFileUploadSample(self, Id, localPath, fileName, directory, contentT
         if partSize <= 5 or partSize > 25:
             raise UploadPartSizeException("Multipart upload partSize must be >5 MB and <=25 MB")
         if tempDir is None:
-            tempDir = mkdtemp()
+            if self.tempdir:
+                tempDir = self.tempdir
+            else:
+                tempDir = mkdtemp()
         bsFile = self.__initiateMultipartFileUploadSample__(Id, fileName, directory, contentType)
         myMpu = mpu(self, localPath, bsFile, processCount, partSize, temp_dir=tempDir)
         return myMpu.upload()
@@ -1495,3 +1530,28 @@ def getResourceProperties(self, resourceType, resourceId):
         headerParams = {}
         return self.__singleRequest__(PropertiesResponse.PropertiesResponse,
                                       resourcePath, method, queryParams, headerParams)
+
+    def getApplications(self, queryPars=None):
+        '''
+        Get details about all apps.
+        Note that each app will only have a single entry, even if it has many versions
+        :param queryPars: query parameters
+        :return: list of model.Application.Application objects
+        '''
+        resourcePath = '/applications'
+        method = 'GET'
+        headerParams = {}
+        queryParams = self._validateQueryParameters(queryPars)
+        return self.__listRequest__(Application.Application, resourcePath, method, queryParams, headerParams)
+
+    def getApplicationById(self, Id):
+        '''
+        Get a single app by ID
+        :return: App object
+        :raises: ServerResponseException if there is no such app
+        '''
+        resourcePath = '/applications/%s' % Id
+        method = 'GET'
+        headerParams = {}
+        queryParams = {}
+        return self.__singleRequest__(Application.Application, resourcePath, method, queryParams, headerParams)
diff --git a/src/BaseSpacePy/api/BaseSpaceException.py b/src/BaseSpacePy/api/BaseSpaceException.py
index c4bba4a..579cffc 100644
--- a/src/BaseSpacePy/api/BaseSpaceException.py
+++ b/src/BaseSpacePy/api/BaseSpaceException.py
@@ -49,43 +49,43 @@ def __str__(self):
 
 class UploadPartSizeException(Exception):
     def __init__(self, value):
-        self.parameter = 'Upload part size invalid: ' + value
+        self.parameter = 'Upload part size is invalid: ' + value
     def __str__(self):
         return repr(self.parameter)
 
 class CredentialsException(Exception):
     def __init__(self, value):
-        self.parameter = 'Error with BaseSpace credentials: ' + value
+        self.parameter = 'Invalid BaseSpace credentials: ' + value
     def __str__(self):
         return repr(self.parameter)
 
 class QueryParameterException(Exception):
     def __init__(self, value):
-        self.parameter = 'Error with query parameter: ' + value
+        self.parameter = 'Invalid query parameter: ' + value
     def __str__(self):
         return repr(self.parameter)
 
 class AppSessionException(Exception):
     def __init__(self, value):
-        self.parameter = 'Error with AppSession: ' + value
+        self.parameter = 'AppSession error: ' + value
     def __str__(self):
         return repr(self.parameter)
 
 class ModelNotSupportedException(Exception):
     def __init__(self, value):
-        self.parameter = 'Model not supported: ' + value
+        self.parameter = 'Unsupported model: ' + value
     def __str__(self):
         return repr(self.parameter)
 
 class OAuthException(Exception):
     def __init__(self, value):
-        self.parameter = 'Error with OAuth: ' + value
+        self.parameter = 'Could not authenticate with OAuth: ' + value
     def __str__(self):
         return repr(self.parameter)
 
 class RestMethodException(Exception):
     def __init__(self, value):
-        self.parameter = 'Error with REST API method: ' + value
+        self.parameter = 'Problem with REST API method: ' + value
     def __str__(self):
         return repr(self.parameter)
     
diff --git a/src/BaseSpacePy/model/Application.py b/src/BaseSpacePy/model/Application.py
index d2b4f02..12a4ac9 100644
--- a/src/BaseSpacePy/model/Application.py
+++ b/src/BaseSpacePy/model/Application.py
@@ -9,5 +9,6 @@ def __init__(self):
             'HrefLogo': 'str',
             'HomepageUri': 'str',
             'ShortDescription': 'str',
-            'DateCreated': 'datetime'            
+            'DateCreated': 'datetime',
+            'VersionNumber': 'str'
         }
diff --git a/src/BaseSpacePy/model/KeyValues.py b/src/BaseSpacePy/model/KeyValues.py
new file mode 100644
index 0000000..c211e5b
--- /dev/null
+++ b/src/BaseSpacePy/model/KeyValues.py
@@ -0,0 +1,13 @@
+class KeyValues(object):
+
+    def __init__(self):
+        self.swaggerTypes = {
+            'Key': 'str',
+            'Values': 'list',
+        }
+
+    def __str__(self):
+        return str(self.Key)
+
+    def __repr__(self):
+        return str(self)
diff --git a/src/BaseSpacePy/model/MultipartFileTransfer.py b/src/BaseSpacePy/model/MultipartFileTransfer.py
index 4555a37..fc5adc3 100644
--- a/src/BaseSpacePy/model/MultipartFileTransfer.py
+++ b/src/BaseSpacePy/model/MultipartFileTransfer.py
@@ -13,18 +13,19 @@
 
 LOGGER = logging.getLogger(__name__)
 
+
 class UploadTask(object):
     '''
     Uploads a piece of a large local file.    
     '''    
-    def __init__(self, api, bs_file_id, piece, total_pieces, local_path, total_size, temp_dir):
+    def __init__(self, api, bs_file_id, piece, total_pieces, local_path, total_size, chunk_size):
         self.api        = api
         self.bs_file_id = bs_file_id  # the BaseSpace File Id
         self.piece      = piece       # piece number 
         self.total_pieces = total_pieces # out of total piece count
         self.local_path = local_path  # the path of the local file to be uploaded, including file name        
         self.total_size = total_size  # total file size of upload, for reporting
-        self.temp_dir   = temp_dir    # temp location to store file chunks for upload
+        self.chunk_size   = chunk_size    # chunk size
         
         # tasks must implement these attributes and execute()
         self.success  = False
@@ -32,29 +33,18 @@ def __init__(self, api, bs_file_id, piece, total_pieces, local_path, total_size,
     
     def execute(self, lock):
         '''
-        Upload a piece of the target file, first splitting the local file into a temp file.
         Calculate md5 of file piece and pass to upload method.
         Lock is not used (but needed since worker sends it for multipart download)
         '''            
         try:
             fname = os.path.basename(self.local_path)
-            # this relies on the way the calling function has split the file
-            # but we still need to pass around the piece numbers because the BaseSpace API needs them
-            # to reassemble the file at the other end
-            # the zfill(4) is to make sure we have a zero padded suffix that split -a 4 -d will make
-            transFile = os.path.join(self.temp_dir, fname + str(self.piece).zfill(4))
-            #cmd = ['split', '-d', '-n', str(self.piece) + '/' + str(self.total_pieces), self.local_path]                        
-            #with open(transFile, "w") as fp:                                                    
-            #    rc = call(cmd, stdout=fp)
-            #    if rc != 0:
-            #        self.sucess = False
-            #        self.err_msg = "Splitting local file failed for piece %s" % str(self.piece)
-            #        return self            
-            with open(transFile, "r") as f:
-                out = f.read()
-                self.md5 = hashlib.md5(out).digest().encode('base64')            
+            chunk_data = ""
+            with open(self.local_path) as fh:
+                fh.seek(self.piece * self.chunk_size)
+                chunk_data = fh.read(self.chunk_size)
+            self.md5 = hashlib.md5(chunk_data).digest().encode('base64')
             try:
-                res = self.api.__uploadMultipartUnit__(self.bs_file_id,self.piece+1,self.md5,transFile)
+                res = self.api.__uploadMultipartUnit__(self.bs_file_id,self.piece+1,self.md5,chunk_data)
             except Exception as e:
                 self.success = False
                 self.err_msg = str(e)                
@@ -65,8 +55,6 @@ def execute(self, lock):
                 else:
                     self.success = False
                     self.err_msg = "Error - empty response from uploading file piece or missing ETag in response"
-            if self.success:
-                os.remove(transFile)
         # capture exception, since unpickleable exceptions may block
         except Exception as e:
             self.success = False
@@ -74,7 +62,7 @@ def execute(self, lock):
         return self
         
     def __str__(self):
-        return 'File piece %d of %d, total file size %s' % (self.piece, self.total_pieces, Utils.readable_bytes(self.total_size))
+        return 'File piece %d of %d, total %s' % (self.piece+1, self.total_pieces, Utils.readable_bytes(self.total_size))
 
 
 class DownloadTask(object):
@@ -180,7 +168,8 @@ def run(self):
             else:                                                       
                 # attempt to run tasks, with retry
                 LOGGER.debug('Worker %s processing task: %s' % (self.name, str(next_task)))
-                for i in xrange(1, self.retries + 1):                        
+                LOGGER.info('%s' % str(next_task))
+                for i in xrange(1, self.retries + 1):
                     if self.halt.is_set():
                         LOGGER.debug('Worker %s exiting, found halt signal' % self.name)
                         self.task_queue.task_done()
@@ -230,7 +219,7 @@ class Executor(object):
     '''
     def __init__(self):                                        
         self.tasks = multiprocessing.JoinableQueue()
-        self.result_queue = multiprocessing.Queue()                        
+        self.result_queue = multiprocessing.Queue()
         self.halt_event = multiprocessing.Event()
         self.lock = multiprocessing.Lock()
     
@@ -284,7 +273,7 @@ class MultipartUpload(object):
     '''
     Uploads a (large) file by uploading file parts in separate processes.    
     '''
-    def __init__(self, api, local_path, bs_file, process_count, part_size, temp_dir):
+    def __init__(self, api, local_path, bs_file, process_count, part_size, logger=None):
         '''
         Create a multipart upload object
         
@@ -293,17 +282,15 @@ def __init__(self, api, local_path, bs_file, process_count, part_size, temp_dir)
         :param bs_file:       the File object of the newly created BaseSpace File to upload 
         :param process_count: the number of process to use for uploading
         :param part_size:     in MB, the size of each uploaded part        
-        :param temp_dir:      temp directory to store file pieces for upload 
         '''
         self.api            = api    
         self.local_path     = local_path    
         self.remote_file    = bs_file
         self.process_count  = process_count
         self.part_size      = part_size
-        self.temp_dir       = temp_dir               
-                                           
+
         self.start_chunk    = 0
-    
+
     def upload(self):
         '''
         Start the upload, then when complete retrieve and return the file object from
@@ -317,36 +304,37 @@ def _setup(self):
         '''
         Determine number of file pieces to upload, add upload tasks to work queue         
         '''                
-        logfile = os.path.join(self.temp_dir, "main.log")
-        total_size = os.path.getsize(self.local_path)        
-        fileCount = int(total_size/(self.part_size*1024*1024)) + 1
+        from math import ceil
+        total_size = os.path.getsize(self.local_path)
+        # round up to get a number of chunks that will be enough for the whole file
+        fileCount = int(ceil(total_size/float(self.part_size*1024*1024)))
 
-        chunk_size = (total_size / fileCount) + 1
+        chunk_size = self.part_size*1024*1024
         assert chunk_size * fileCount > total_size
 
-        fname = os.path.basename(self.local_path)
-        prefix = os.path.join(self.temp_dir, fname)
+        # fname = os.path.basename(self.local_path)
+        # prefix = os.path.join(self.temp_dir, fname)
         # -a 4  always use 4 digit sufixes, to make sure we can predict the filenames
         # -d    use digits as suffixes, not letters
         # -b    chunk size (in bytes)
-        cmd = ['split', '-a', '4', '-d', '-b', str(chunk_size), self.local_path, prefix]
-        rc = call(cmd)
-        if rc != 0:
-            err_msg = "Splitting local file failed: %s" % str.local_path
-            raise MultiProcessingTaskFailedException(err_msg)
+        # cmd = ['split', '-a', '4', '-d', '-b', str(chunk_size), self.local_path, prefix]
+        # rc = call(cmd)
+        # if rc != 0:
+        #     err_msg = "Splitting local file failed: %s" % self.local_path
+        #     raise MultiProcessingTaskFailedException(err_msg)
 
         self.exe = Executor()                    
         for i in xrange(self.start_chunk, fileCount):
-            t = UploadTask(self.api, self.remote_file.Id, i, fileCount, self.local_path, total_size, self.temp_dir)            
+            t = UploadTask(self.api, self.remote_file.Id, i, fileCount, self.local_path, total_size, chunk_size)
             self.exe.add_task(t)            
         self.exe.add_workers(self.process_count)
         self.task_total = fileCount - self.start_chunk + 1                                                
 
         LOGGER.info("Total File Size %s" % Utils.readable_bytes(total_size))
         LOGGER.info("Using File Part Size %d MB" % self.part_size)
-        LOGGER.info("Processes %d" % self.process_count)
-        LOGGER.info("File Chunk Count %d" % self.task_total)
-        LOGGER.info("Start Chunk %d" % self.start_chunk)    
+        LOGGER.debug("Processes %d" % self.process_count)
+        LOGGER.debug("File Chunk Count %d" % self.task_total)
+        LOGGER.debug("Start Chunk %d" % self.start_chunk)
 
     def _start_workers(self):
         '''
@@ -435,11 +423,11 @@ def _setup(self):
         self.exe.add_workers(self.process_count)        
         self.task_total = self.file_count - self.start_chunk + 1                                                
                                  
-        LOGGER.info("Total File Size %s" % Utils.readable_bytes(total_bytes))
-        LOGGER.info("Using File Part Size %s MB" % str(self.part_size))
-        LOGGER.info("Processes %d" % self.process_count)
-        LOGGER.info("File Chunk Count %d" % self.file_count)
-        LOGGER.info("Start Chunk %d" % self.start_chunk)
+        LOGGER.debug("Total File Size %s" % Utils.readable_bytes(total_bytes))
+        LOGGER.debug("Using File Part Size %s MB" % str(self.part_size))
+        LOGGER.debug("Processes %d" % self.process_count)
+        LOGGER.debug("File Chunk Count %d" % self.file_count)
+        LOGGER.debug("Start Chunk %d" % self.start_chunk)
                             
     def _start_workers(self):
         '''
diff --git a/src/BaseSpacePy/model/Project.py b/src/BaseSpacePy/model/Project.py
index 80f5cef..75b6514 100644
--- a/src/BaseSpacePy/model/Project.py
+++ b/src/BaseSpacePy/model/Project.py
@@ -58,10 +58,11 @@ def getAppResults(self, api, queryPars=None, statuses=None):
         :param api: An instance of BaseSpaceAPI
         :param queryPars: An (optional) object of type QueryParameters for custom sorting and filtering
         :param statuses: An optional list of statuses, eg. 'complete'
+        :return: list of AppResult objects
         '''
         self.isInit()
         return api.getAppResultsByProject(self.Id, queryPars=queryPars, statuses=statuses)
-        
+
     def getSamples(self, api, queryPars=None):
         '''
         Returns a list of Sample objects.
diff --git a/src/BaseSpacePy/model/PropertyMap.py b/src/BaseSpacePy/model/PropertyMap.py
index 279bba7..a1344ab 100644
--- a/src/BaseSpacePy/model/PropertyMap.py
+++ b/src/BaseSpacePy/model/PropertyMap.py
@@ -7,7 +7,7 @@ def __init__(self):
             'Href': 'str',
             'Name': 'str',
             'Description': 'str',
-            'Items': 'list',           
+            'Content': 'list',
         }
 
     def __str__(self):
diff --git a/src/BaseSpacePy/model/QueryParameters.py b/src/BaseSpacePy/model/QueryParameters.py
index ddf833e..f5570e2 100644
--- a/src/BaseSpacePy/model/QueryParameters.py
+++ b/src/BaseSpacePy/model/QueryParameters.py
@@ -2,6 +2,7 @@
 from BaseSpacePy.api.BaseSpaceException import UndefinedParameterException, UnknownParameterException, IllegalParameterException, QueryParameterException
 
 legal = {'Statuses': [],
+         'Status':[],
          'SortBy': ['Id', 'Name', 'DateCreated', 'Path', 'Position'],
          'Extensions': [],
          #'Extensions': ['bam', 'vcf'],
@@ -11,7 +12,10 @@
          'Name': [], 
          'StartPos':[], 
          'EndPos':[], 
-         'Format':[]
+         'Format':[],
+         'include':[],
+         'propertyFilters':[],
+         'userCreatedBy':[]
          #'Format': ['txt', 'json', 'vcf'], 
          }
 
diff --git a/src/BaseSpacePy/model/Token.py b/src/BaseSpacePy/model/Token.py
new file mode 100644
index 0000000..55ea5f9
--- /dev/null
+++ b/src/BaseSpacePy/model/Token.py
@@ -0,0 +1,10 @@
+class Token(object):
+    def __init__(self):
+        self.swaggerTypes = {
+            'Scopes': 'list',
+            'DateCreated': 'datetime',
+            'UserResourceOwner': 'User',
+            'Application': 'Application',
+            'AccessToken': 'str'
+        }
+    
diff --git a/src/BaseSpacePy/model/__init__.py b/src/BaseSpacePy/model/__init__.py
index f78aadf..7259b23 100644
--- a/src/BaseSpacePy/model/__init__.py
+++ b/src/BaseSpacePy/model/__init__.py
@@ -1,5 +1,6 @@
 
 __all__= [
+ 'Token',
  'ListResponse',
  'ResponseStatus',
  'File',
@@ -72,4 +73,5 @@
  'RunResponse',
  'Run',
  'MultipartFileTransfer',
+ 'KeyValues',
  ]
diff --git a/src/setup.cfg b/src/setup.cfg
new file mode 100644
index 0000000..0ac0f47
--- /dev/null
+++ b/src/setup.cfg
@@ -0,0 +1,6 @@
+[bdist_rpm]
+
+requires = python >= 2.6
+           python-dateutil
+           python-requests
+no-autoreq = yes
diff --git a/src/setup.py b/src/setup.py
index d98dca5..fb41500 100755
--- a/src/setup.py
+++ b/src/setup.py
@@ -21,29 +21,25 @@
     from distutils.core import setup
 
 
-setup(name='BaseSpacePy',
+setup(name='basespace-python-sdk',
       description='A Python SDK for connecting to Illumina BaseSpace data',
       author='Illumina',
-      version='0.3',
+      version='0.4.2',
       long_description="""
 BaseSpacePy is a Python based SDK to be used in the development of Apps and scripts for working with
 Illumina's BaseSpace cloud-computing solution for next-gen sequencing data analysis.
 The primary purpose of the SDK is to provide an easy-to-use Python environment enabling developers
 to authenticate a user, retrieve data, and upload data/results from their own analysis to BaseSpace.""",
-      author_email='',
+      author_email='techsupport@illumina.com',
       packages=['BaseSpacePy.api','BaseSpacePy.model','BaseSpacePy'],
       package_dir={'BaseSpacePy' : os.path.join(os.path.dirname(__file__),'BaseSpacePy')},
-      requires=['pycurl','dateutil'],
+      install_requires=['python-dateutil','requests'],
       zip_safe=False,
 )
 
 
 # Warn use if dependent packages aren't installed
 #try:
-#    import pycurl
-#except:
-#    print "WARNING - please install required package 'pycurl'"
-#try:
 #    import dateutil
 #except:
 #    print "WARNING - please install required package 'python-dateutil'"
diff --git a/test/unit_tests.py b/test/unit_tests.py
index d71e95d..be807ac 100644
--- a/test/unit_tests.py
+++ b/test/unit_tests.py
@@ -21,8 +21,8 @@
 
 # Dependencies:
 # ============
-# 1. Create a profile named 'unit_tests' in ~/.basespacepy.cfg that has the credentials for an app on https://portal-hoth.illumina.com;
-#    (there should also be a 'DEFALT' profile in the config file)
+# 1. Create a config file in ~/.basespace/unit_tests.cfg that has the credentials for an app on https://portal-hoth.illumina.com;
+#    you can do this with: bs -c unit_tests authenticate --api-server https://api.cloud-hoth.illumina.com/
 # 2. Import the following data from Public Dataset 'MiSeq B. cereus demo data' on cloud-hoth.illumina.com:
 #    2.a.  Project name 'BaseSpaceDemo' (Id 596596), and
 #    2.b.  Run name 'BacillusCereus' (Id 555555)
@@ -40,6 +40,7 @@
            'file_large_md5': '9267236a2d870da1d4cb73868bb51b35', # for file id 9896135 
            # for upload tests
            'file_small_upload': 'data/test.small.upload.txt',
+           'file_small_upload_contents': open('data/test.small.upload.txt').read(),
            'file_large_upload': 'data/BC-12_S12_L001_R2_001.fastq.gz',
            'file_small_upload_size': 11,
            'file_large_upload_size': 57995799,
@@ -201,7 +202,7 @@ def test__uploadMultipartUnit__(self):
             Id = file.Id,
             partNumber = 1,
             md5 = md5,
-            data = tconst['file_small_upload'])
+            data = tconst['file_small_upload_contents'])
         self.assertNotEqual(response, None, 'Upload part failure will return None')
         self.assertTrue('ETag' in response['Response'], 'Upload part success will contain a Response dict with an ETag element')
             
@@ -220,7 +221,7 @@ def test__finalizeMultipartFileUpload__(self):
             Id = file.Id,
             partNumber = 1,
             md5 = md5,
-            data = tconst['file_small_upload'])
+            data = tconst['file_small_upload_contents'])
         final_file = self.api.__finalizeMultipartFileUpload__(file.Id)
         self.assertEqual(final_file.UploadStatus, 'complete')
 
@@ -319,7 +320,6 @@ def testMultiPartFileUpload(self):
             fileName=fileName, 
             directory=testDir,                          
             contentType=tconst['file_large_upload_content_type'],
-            tempDir=None, 
             processCount = 4,
             partSize= 10, # MB, chunk size            
             #tempDir = args.temp_dir
@@ -1495,8 +1495,7 @@ def testGetAppSessionPropertiesById(self):
 
     def testGetAppSessionPropertiesByIdWithQp(self):
         props = self.api.getAppSessionPropertiesById(self.ssn.Id, qp({'Limit':1}))
-        self.assertTrue(any((prop.Items[0].Id == self.ar.Id) for prop in props.Items if prop.Name == "Output.AppResults"))
-        self.assertEqual(len(props.Items), 1)         
+        self.assertEqual(len(props.Items), 1)
 
     def testGetAppSessionPropertyByName(self):
         prop = self.api.getAppSessionPropertyByName(self.ssn.Id, 'Output.AppResults')
@@ -1510,12 +1509,20 @@ def testGetAppSessionPropertyByNameWithQp(self):
 
     def testGetAppSessionInputsById(self):
         props = self.api.getAppSessionInputsById(self.ssn.Id)
-        self.assertEqual(len(props), 0)
+        self.assertEqual(len(props), 1)
+        self.assertEqual("Samples" in props, True)
+        self.assertEqual(len(props["Samples"].Items), 0)
+        # NB: these have changed from previous versions of the unit tests
+        # because it looks like appsessions created through the API now have an (empty) input samples list by default
         # TODO can't test this easily since self-created ssn don't have inputs. Add POST properties for ssns, and manually add an 'Input.Test' property, then test for it?
     
     def testGetAppSessionInputsByIdWithQp(self):
         props = self.api.getAppSessionInputsById(self.ssn.Id, qp({'Limit':1}))
-        self.assertEqual(len(props), 0)
+        self.assertEqual(len(props), 1)
+        self.assertEqual("Samples" in props, True)
+        self.assertEqual(len(props["Samples"].Items), 0)
+        # NB: these have changed from previous versions of the unit tests
+        # because it looks like appsessions created through the API now have an (empty) input samples list by default
         # TODO same as test above
 
     def testSetAppSessionState_UpdatedStatus(self):
@@ -1679,14 +1686,14 @@ def setUp(self):
         
     def test_setCredentials_AllFromProfile(self):                                                            
         creds = self.api._setCredentials(clientKey=None, clientSecret=None,
-            apiServer=None, apiVersion=None, appSessionId='', accessToken='',
+            apiServer=None, appSessionId='', apiVersion=self.api.version, accessToken='',
             profile=self.profile)
-        self.assertEqual(creds['clientKey'], self.api.key)
-        self.assertEqual('profile' in creds, True)
-        self.assertEqual(creds['clientSecret'], self.api.secret)
-        self.assertEqual(urljoin(creds['apiServer'], creds['apiVersion']), self.api.apiClient.apiServerAndVersion)
-        self.assertEqual(creds['apiVersion'], self.api.version)
-        self.assertEqual(creds['appSessionId'], self.api.appSessionId)
+        # self.assertEqual(creds['clientKey'], self.api.key)
+        # self.assertEqual('profile' in creds, True)
+        # self.assertEqual(creds['clientSecret'], self.api.secret)
+        self.assertEqual(urljoin(creds['apiServer'], self.api.version), self.api.apiClient.apiServerAndVersion)
+        # self.assertEqual(creds['apiVersion'], self.api.version)
+        # self.assertEqual(creds['appSessionId'], self.api.appSessionId)
         self.assertEqual(creds['accessToken'], self.api.getAccessToken())
 
     def test_setCredentials_AllFromConstructor(self):                                                            
@@ -1705,7 +1712,7 @@ def test_setCredentials_MissingConfigCredsException(self):
         # Danger: if this test fails unexpectedly, the config file may not be renamed back to the original name
         # 1) mv current .basespacepy.cfg, 2) create new with new content,
         # 3) run test, 4) erase new, 5) mv current back        
-        cfg = os.path.expanduser('~/.basespacepy.cfg')
+        cfg = os.path.expanduser('~/.basespace/unit_tests.cfg')
         tmp_cfg = cfg + '.unittesting.donotdelete'
         shutil.move(cfg, tmp_cfg)                
         new_cfg_content = ("[" + self.profile + "]\n"
@@ -1724,42 +1731,43 @@ def test__setCredentials_DefaultsForOptionalArgs(self):
         # Danger: if this test fails unexpectedly, the config file may not be renamed back to the original name
         # 1) mv current .basespacepy.cfg, 2) create new with new content,
         # 3) run test, 4) erase new, 5) mv current back
-        cfg = os.path.expanduser('~/.basespacepy.cfg')
+        cfg = os.path.expanduser('~/.basespace/unit_tests.cfg')
         tmp_cfg = cfg + '.unittesting.donotdelete'
         shutil.move(cfg, tmp_cfg)                
-        new_cfg_content = ("[" + self.profile + "]\n"                       
+        new_cfg_content = ("[DEFAULT]\n"
                           "clientKey=test\n"
                           "clientSecret=test\n"                                                    
                           "apiServer=test\n"
-                          "apiVersion=test\n")                          
+                          "apiVersion=test\n"
+                          "accessToken=test\n")
         with open(cfg, "w") as f:
             f.write(new_cfg_content)    
         creds = self.api._setCredentials(clientKey=None, clientSecret=None,
-                apiServer=None, apiVersion=None, appSessionId='', accessToken='',
+                apiServer=None, apiVersion=self.api.version, appSessionId='', accessToken='',
                 profile=self.profile)
         self.assertEqual(creds['appSessionId'], '')
-        self.assertEqual(creds['accessToken'], '')
+        self.assertEqual(creds['accessToken'], 'test')
         os.remove(cfg)
         shutil.move(tmp_cfg, cfg)        
 
     def test__getLocalCredentials(self):                                                            
         creds = self.api._getLocalCredentials(profile='unit_tests')
         self.assertEqual('name' in creds, True)
-        self.assertEqual('clientKey' in creds, True)
-        self.assertEqual('clientSecret' in creds, True)
+        # self.assertEqual('clientKey' in creds, True)
+        # self.assertEqual('clientSecret' in creds, True)
         self.assertEqual('apiServer' in creds, True)
-        self.assertEqual('apiVersion' in creds, True)
-        self.assertEqual('appSessionId' in creds, True)
+        # self.assertEqual('apiVersion' in creds, True)
+        # self.assertEqual('appSessionId' in creds, True)
         self.assertEqual('accessToken' in creds, True)
 
     def test__getLocalCredentials_DefaultProfile(self):
         creds = self.api._getLocalCredentials(profile=self.profile)
         self.assertEqual('name' in creds, True)
-        self.assertEqual('clientKey' in creds, True)
-        self.assertEqual('clientSecret' in creds, True)
+#        self.assertEqual('clientKey' in creds, True)
+#        self.assertEqual('clientSecret' in creds, True)
         self.assertEqual('apiServer' in creds, True)
-        self.assertEqual('apiVersion' in creds, True)
-        self.assertEqual('appSessionId' in creds, True)
+        # self.assertEqual('apiVersion' in creds, True)
+#        self.assertEqual('appSessionId' in creds, True)
         self.assertEqual('accessToken' in creds, True)
 
     def test__getLocalCredentials_MissingProfile(self):                                                        
@@ -1922,12 +1930,12 @@ def setUp(self):
         
     def test__init__(self):
         creds = self.api._getLocalCredentials(profile='unit_tests')
-        self.assertEqual(creds['appSessionId'], self.api.appSessionId)
-        self.assertEqual(creds['clientKey'], self.api.key)
-        self.assertEqual(creds['clientSecret'], self.api.secret)
+        # self.assertEqual(creds['appSessionId'], self.api.appSessionId)
+        # self.assertEqual(creds['clientKey'], self.api.key)
+        # self.assertEqual(creds['clientSecret'], self.api.secret)
         self.assertEqual(creds['apiServer'], self.api.apiServer)
-        self.assertEqual(creds['apiVersion'], self.api.version)
-        self.assertEqual(creds['name'], self.api.profile)
+        # self.assertEqual(creds['apiVersion'], self.api.version)
+        # self.assertEqual(creds['name'], self.api.profile)
         self.assertEqual(creds['apiServer'].replace('api.',''), self.api.weburl)
 
 class TestBaseAPIMethods(TestCase):
@@ -1942,7 +1950,7 @@ def test__init__(self):
         accessToken = "123"
         apiServerAndVersion = "http://api.tv"
         timeout = 50
-        bapi = BaseAPI(accessToken, apiServerAndVersion, timeout)
+        bapi = BaseAPI(accessToken, apiServerAndVersion, timeout=timeout)
         self.assertEqual(bapi.apiClient.apiKey, accessToken)
         self.assertEqual(bapi.apiClient.apiServerAndVersion, apiServerAndVersion)
         self.assertEqual(bapi.apiClient.timeout, timeout)
@@ -1986,15 +1994,6 @@ def test__singleRequest__WithForcePost(self):
             queryParams, headerParams, postData=postData, forcePost=1)
         self.assertTrue(hasattr(file, 'Id'), 'Successful force post should return file object with Id attribute here')                            
 
-    def test__singleRequest__Verbose(self):
-        # get current user
-        resourcePath = '/users/current'        
-        method = 'GET'        
-        queryParams = {}
-        headerParams = {}
-        user = self.bapi.__singleRequest__(UserResponse.UserResponse, resourcePath, method, queryParams, headerParams, verbose=True)
-        self.assertTrue(hasattr(user, 'Id'))
-
     @skip("Not sure how to test this, requires no response from api server")
     def test__singleRequest__NoneResponseException(self):
         pass
@@ -2027,16 +2026,6 @@ def test__listRequest__(self):
         self.assertTrue(isinstance(runs, list))
         self.assertTrue(hasattr(runs[0], "Id"))
 
-    def test__listRequest__Verbose(self):
-        # get current user
-        resourcePath = '/users/current/runs'        
-        method = 'GET'        
-        queryParams = {}
-        headerParams = {}
-        runs = self.bapi.__listRequest__(Run.Run, resourcePath, method, queryParams, headerParams, verbose=True)
-        self.assertTrue(isinstance(runs, list))
-        self.assertTrue(hasattr(runs[0], "Id"))
-
     @skip("Not sure how to test this, requires no response from api server")
     def test__listRequest__NoneResponseException(self):
         pass
@@ -2157,8 +2146,8 @@ def test__putCall__(self):
         resourcePath                 = resourcePath.replace('{Id}', file.Id)
         resourcePath                 = resourcePath.replace('{partNumber}', str(1))        
         headerParams                 = {'Content-MD5': md5}
-        transFile                    = tconst['file_small_upload']
-        putResp = self.apiClient.__putCall__(resourcePath=self.apiClient.apiServerAndVersion + resourcePath, headers=headerParams, transFile=transFile)
+        data                         = tconst['file_small_upload_contents']
+        putResp = self.apiClient.__putCall__(resourcePath=self.apiClient.apiServerAndVersion + resourcePath, headers=headerParams, data=data)
         #print "RESPONSE is: " + putResp        
         jsonResp =  putResp.split()[-1] # normally done in callAPI()
         dictResp = json.loads(jsonResp)
@@ -2254,8 +2243,8 @@ def testCallAPI_PUT(self):
         resourcePath                 = resourcePath.replace('{partNumber}', str(1))        
         headerParams                 = {'Content-MD5': md5}
         queryParams                  = {} # not used for PUT calls
-        transFile                    = tconst['file_small_upload']
-        dictResp = self.apiClient.callAPI(resourcePath, method, queryParams, postData=transFile, headerParams=headerParams)
+        data                         = tconst['file_small_upload_contents']
+        dictResp = self.apiClient.callAPI(resourcePath, method, queryParams, postData=data, headerParams=headerParams)
         self.assertTrue('Response' in dictResp, 'Successful force post should return json with Response attribute: ' + str(dictResp))       
         self.assertTrue('ETag' in dictResp['Response'], 'Successful force post should return json with Response with Id attribute: ' + str(dictResp))                                                                    
 
@@ -2283,7 +2272,7 @@ def testCallAPI_HandleHttpError_ForGET(self):
         self.assertTrue('ResponseStatus' in dictResp, 'response is: ' + str(dictResp))       
         self.assertTrue('ErrorCode' in dictResp['ResponseStatus'])
         self.assertTrue('Message' in dictResp['ResponseStatus'])
-        self.assertEqual(dictResp['ResponseStatus']['Message'], 'Unauthorized')
+        self.assertTrue('Unrecognized access token' in dictResp['ResponseStatus']['Message'])
 
     def testCallAPI_HandleHttpError_ForPOST(self):
         # bad access token throws 401 Error and HTTPError exception by urllib2;  create a project uses POST                                 
@@ -2297,7 +2286,7 @@ def testCallAPI_HandleHttpError_ForPOST(self):
         self.assertTrue('ResponseStatus' in dictResp, 'response is: ' + str(dictResp))       
         self.assertTrue('ErrorCode' in dictResp['ResponseStatus'])
         self.assertTrue('Message' in dictResp['ResponseStatus'])
-        self.assertEqual(dictResp['ResponseStatus']['Message'], 'Unauthorized')
+        self.assertTrue('Unrecognized access token' in dictResp['ResponseStatus']['Message'])
 
     @skip('Not sure how to cause json returned from server to be malformed, in order to cause an exception in json parsing')
     def testCallAPI_JsonParsingException(self):