importXLSX.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import os
  2. import pandas
  3. import sys
  4. import importlib
  5. import re
  6. import datetime
  7. import chardet
  8. import json
  9. import math
  10. #same directory!
  11. import labkeyInterface
  12. import labkeyDatabaseBrowser
  13. import labkeyFileBrowser
  14. def connectDB(server):
  15. net=labkeyInterface.labkeyInterface()
  16. qfile='{}.json'.format(server)
  17. fconfig=os.path.join(os.path.expanduser('~'),'.labkey',qfile)
  18. net.init(fconfig)
  19. net.getCSRF()
  20. return labkeyDatabaseBrowser.labkeyDB(net)
  21. def getDB(pars):
  22. try:
  23. return pars['db']
  24. except KeyError:
  25. pass
  26. server=pars.get('server','onko-nix')
  27. db=connectDB(server)
  28. pars['db']=db
  29. return db
  30. def getFields(pars):
  31. project=pars.get('project','DCIS/Study')
  32. schema=pars.get('schema','demographics')
  33. query=pars.get('query','demographics')
  34. db=getDB(pars)
  35. #data on query structure are both in queryDesign and selectRows/metadata content
  36. dsgn=db.getQueryDesign(project,schema,query)
  37. dsgnFields={f['name']:f for f in dsgn['fields']}
  38. ds=db.selectRows(project,schema,query,[])
  39. mdFields={x['name']:x for x in ds['metaData']['fields']}
  40. #in principle, any property from mdFields could get copied
  41. #to content reported from dsgnFields
  42. #try/except to robustify against missing components
  43. copyFields=['lookup']
  44. for f in dsgnFields:
  45. try:
  46. dsgnFields[f].update({x:mdFields[f][x] for x in copyFields})
  47. except KeyError:
  48. pass
  49. return dsgnFields
  50. def getAlias(fields):
  51. fieldMap={}
  52. for f in fields.values():
  53. aliasList=getAliasList(f['importAliases'])
  54. fieldMap.update({x:f['name'] for x in aliasList})
  55. return fieldMap
  56. def invertMap(qmap):
  57. return {qmap[x]:x for x in qmap}
  58. def getVariables(fields,fieldType='LOOKUP',fieldName=None):
  59. #get list of variables of particular type to help manage import
  60. #if type is LOOKUP also return data on lookup query
  61. if fieldName:
  62. return {fieldName:fields[fieldName]['lookupQuery']}
  63. if fieldType=='LOOKUP':
  64. return {f['name']:f['lookupQuery'] for f in fields.values() \
  65. if f['lookupQuery']}
  66. if fieldType=='DATE':
  67. return {f['name']:fieldType for f in fields.values() \
  68. if f['rangeURI'].find('dateTime')>-1}
  69. if fieldType=='DOUBLE':
  70. return {f['name']:fieldType for f in fields.values() \
  71. if f['rangeURI'].find('double')>-1}
  72. return {}
  73. def getLookupMap(pars,fields,fieldName):
  74. #get possible values of categorical variables/factors from labkey
  75. try:
  76. lookup=fields[fieldName]['lookup']
  77. except KeyError:
  78. print(fields[fieldName])
  79. raise KeyError(f'Could not find lookup for {fieldName}')
  80. schema=lookup['schemaName']
  81. query=lookup['queryName']
  82. key=lookup['keyColumn']
  83. val=lookup['displayColumn']
  84. project=pars['project']
  85. db=getDB(pars)
  86. ds=db.selectRows(project,schema,query,[])
  87. cMap={r[val]:r[key] for r in ds['rows']}
  88. return cMap
  89. def parseLookup(lookup,qv):
  90. #parse/convert lookup
  91. #is it key?
  92. try:
  93. return lookup[qv]
  94. except KeyError:
  95. pass
  96. if pandas.isna(qv):
  97. return qv
  98. try:
  99. qv=qv.item()
  100. qv=str(qv)
  101. return lookup[qv]
  102. except AttributeError:
  103. pass
  104. qv=qv.replace('Č','C')
  105. return lookup[qv]
  106. def asKey(qv):
  107. if not qv:
  108. return qv
  109. try:
  110. return int(qv)
  111. except (TypeError,ValueError):
  112. print(f'Failed to parse {qv} as key')
  113. return None
  114. def parseDate(qv):
  115. if not qv:
  116. return qv
  117. #from xls format to native python format
  118. fmts=['datetime','pandas','%d.%m.%y','%Y-%m-%d %H:%M:%S.%f',\
  119. '%d/%m/%Y','%Y-%m-%d %H:%M:%S']
  120. usedFmt=None
  121. for fmt in fmts:
  122. usedFmt=fmt
  123. try:
  124. if fmt=='pandas':
  125. #print(f'Trying {qv} as pandas.Timestamp')
  126. date=pandas.Timestamp.to_pydatetime(qv)
  127. elif fmt=='datetime':
  128. #print(f'Trying {qv} as datetime.datetime')
  129. if not isinstance(qv,datetime.datetime):
  130. raise TypeError('Not a datetime object')
  131. date=qv
  132. else:
  133. #print(f'Trying {qv} with {fmt}')
  134. date=datetime.datetime.strptime(qv,fmt)
  135. break
  136. except TypeError:
  137. #print('Failed (type): {}'.format(type(qv)))
  138. continue
  139. except ValueError:
  140. #print('Failed (value)')
  141. continue
  142. #sometimes parsing fails
  143. try:
  144. return date.isoformat()
  145. except UnboundLocalError:
  146. print (f'Failed to parse {qv} as date, used {usedFmt}')
  147. return None
  148. def parseDouble(qv):
  149. try:
  150. return float(qv)
  151. except (ValueError,TypeError):
  152. return None
  153. #m
  154. def setMissingLookupValues(filename,xlsFieldName,project,lookup,\
  155. labkeyFieldName=None,dryRun=True):
  156. #list all possible values for a field
  157. #perhaps needs to be updated
  158. df=pandas.read_excel(filename)
  159. vars=df.columns
  160. vals=set([df.at[r,xlsFieldName] for r in df.index if not pandas.isna(df.at[r,xlsFieldName])])
  161. try:
  162. vals={v.item() for v in vals}
  163. except AttributeError:
  164. pass
  165. print(vals)
  166. if not labkeyFieldName:
  167. labkeyFieldName=xlsFieldName
  168. db=connectDB('onko-nix')
  169. ds=db.selectRows(project,'lists',lookup,[])
  170. #only record values from labkey (should be unique anyhow)
  171. setVals=set([r[labkeyFieldName] for r in ds['rows']])
  172. off=len(list(setVals))
  173. missing=sorted(list(vals-setVals))
  174. #print('Missing {}'.format(missing))
  175. n=len(missing)
  176. entries=[{'Key':'{}'.format(i+1+off),columnName:missing[i]} \
  177. for i in range(n)]
  178. print(entries)
  179. if dryRun:
  180. return
  181. db.modifyRows('insert',project,'lists',lookup,entries)
  182. def getAliasList(x):
  183. #split aliases by comma, taking into account quotation marks,
  184. #where commas are ignored
  185. if not x:
  186. return []
  187. #sophisticated spliting that ignores commas in double (and single!) quotes
  188. ar=re.split(''',(?=(?:[^'"]|'[^']*'|"[^"]*")*$)''', x)
  189. #remove the quotes afterwards
  190. ar=[s.replace('"','') for s in ar]
  191. ar=[s.strip() for s in ar]
  192. return ar
  193. def printErr(resp):
  194. #print error from server response
  195. try:
  196. print(resp['exception'])
  197. except KeyError:
  198. pass
  199. def findAlias(v,alias):
  200. #find matchng alias for field v from XLS
  201. try:
  202. return alias[v.strip()]
  203. except KeyError:
  204. pass
  205. #remove .N qualifiers, attach to the end by pandas.read_excel
  206. v=re.sub(r'(.*)\.[0-9]*$',r'\1',v.strip())
  207. try:
  208. return alias[v]
  209. except KeyError:
  210. pass
  211. return None
  212. def getSequenceNum(pMap,id):
  213. #updates pMap and return seqnum for this entry
  214. try:
  215. n=len(pMap[id])
  216. except KeyError:
  217. n=0
  218. seqNum=n+1
  219. if n==0:
  220. pMap[id]=[seqNum]
  221. else:
  222. pMap[id].append(seqNum)
  223. return seqNum
  224. def entryInList(r,entries):
  225. #is entry in list
  226. candidates=[x for x in entries if entriesMatch(x,r)]
  227. return len(candidates)>0
  228. def entriesMatch(x,r):
  229. #do a pair of entries match? Should we update data from one over the other?
  230. matchFields=['ParticipantId','SequenceNum']
  231. for f in matchFields:
  232. if x[f]!=r[f]:
  233. return False
  234. return True
  235. def validate(qv):
  236. #rough checks on value read from XLSX
  237. #NAN
  238. try:
  239. if math.isnan(qv):
  240. return None
  241. except TypeError:
  242. pass
  243. #NAD
  244. if pandas.isnull(qv):
  245. return None
  246. return qv
  247. #defaults for modify, getId and convertLookup
  248. def modify(qv,columnName):
  249. return qv
  250. def getId(df,r):
  251. try:
  252. rawId=str(df.at[r,'ID'])
  253. except KeyError:
  254. msg='Getting id from field ID field. '
  255. msg+=' Overload getId function with getId=getIdFnc in importData'
  256. print(msg)
  257. raise KeyError
  258. return rawId.replace(' ','')
  259. def convertLookup(xlsColumnName):
  260. return True
  261. def importData(pars,filename,getId=getId,modify=modify,\
  262. convertLookup=convertLookup,dryRun=True,debug=True):
  263. #master routine that imports data based on pars,
  264. #applies user supplied functions modify, convertLookup and get Id and
  265. #updates relevant database
  266. #some useful fields from pars (d is for default value)
  267. # - skiprows removes irelevant rows (number, d: 0)
  268. # - usecols is EXCEL like notations for cols taken (identifer, d: None for all)
  269. # - sheet_name selects sheet for import (number, d:0 for first sheet)
  270. # - seqNumOffset specify visit/sequenceNum offset (number, d:0 will result in 1)
  271. # - project - labkey project
  272. # - schema - labkey schema (list/study, d: study)
  273. # - query - labkey query
  274. skiprows=pars.get('skiprows',0)
  275. usecols=pars.get('usecols',None)
  276. sheet_name=pars.get('sheet_name',0)
  277. #set this is as sequenceNum for entries, or initial seqNum if more than a single entry is in the dataset
  278. seqNumOffset=pars.get('seqNumOffset',0)
  279. fields=getFields(pars)
  280. lookupVars=getVariables(fields,fieldType='LOOKUP')
  281. dateVars=getVariables(fields,fieldType='DATE')
  282. doubleVars=getVariables(fields,fieldType='DOUBLE')
  283. usecols=pars.get('usecols',None)
  284. #convert dates to list
  285. dateVars=list(dateVars.keys())
  286. print(f'dateVars: {dateVars}')
  287. lookupMap={f:getLookupMap(pars,fields,f) for f in lookupVars}
  288. if debug:
  289. print(lookupMap)
  290. alias=getAlias(fields)
  291. print(f'aliases: {alias}')
  292. df=pandas.read_excel(filename,sheet_name=sheet_name,skiprows=skiprows,\
  293. usecols=usecols)
  294. vars=df.columns
  295. print(vars)
  296. pMap={}
  297. print('Index: {}'.format(len(df.index)))
  298. idx=df.index #for all
  299. if debug:
  300. idx=df.index[0:10] #for debug
  301. entries=[]
  302. for r in idx:
  303. id=getId(df,r)
  304. entry={}
  305. entry['ParticipantId']=id
  306. for v in vars:
  307. qv=validate(df.at[r,v])
  308. qv=modify(qv,v)
  309. f=findAlias(v,alias)
  310. if not f:
  311. continue
  312. if f in lookupMap:
  313. if convertLookup(v):
  314. qv=parseLookup(lookupMap[f],qv)
  315. else:
  316. qv=asKey(qv)
  317. if f in dateVars:
  318. qv=parseDate(qv)
  319. if f in doubleVars:
  320. qv=parseDouble(qv)
  321. try:
  322. numpyType=qv.dtype
  323. qv=qv.item()
  324. except AttributeError:
  325. pass
  326. entry[f]=qv
  327. #print('{}:{}/{}'.format(f,qv,type(qv)))
  328. seqNum=getSequenceNum(pMap,id)
  329. entry['SequenceNum']=seqNum+seqNumOffset
  330. entries.append(entry)
  331. #for p in pMap:
  332. # print('{}: {}'.format(p,len(pMap[p])))
  333. print(entries)
  334. #delete previous incarnations
  335. loadSafely(pars,entries,dryRun=dryRun)
  336. def loadSafely(pars,entries,keyColumn=None,dryRun=True):
  337. #allow additional keys in keyColumn
  338. db=getDB(pars)
  339. project=pars.get('project','DCIS/Study')
  340. schema=pars.get('schema','demographics')
  341. query=pars.get('query','demographics')
  342. updateRows=[]
  343. insertRows=[]
  344. selVal=['ParticipantId','SequenceNum']
  345. if keyColumn:
  346. selVal.append(keyColumn)
  347. for entry in entries:
  348. qFilter=[{'variable':v,'value':'{}'.format(entry[v]),'oper':'eq'} for v in selVal]
  349. ds=db.selectRows(project,schema,query,qFilter)
  350. if len(ds['rows'])>0:
  351. r=ds['rows'][0]
  352. r.update(entry)
  353. updateRows.append(r)
  354. else:
  355. insertRows.append(entry)
  356. n=len(updateRows)
  357. print(f'Updating {n} entries')
  358. if n and not dryRun:
  359. printErr(db.modifyRows('update',project,schema,query,updateRows))
  360. n=len(insertRows)
  361. print(f'Inserting {n} entries')
  362. if n and not dryRun:
  363. printErr(db.modifyRows('insert',project,schema,query,insertRows))