Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3# 

4# Base class for all FHIR elements. 

5 

6import sys 

7import logging 

8 

9logger = logging.getLogger(__name__) 

10 

11 

12class FHIRValidationError(Exception): 

13 """ Exception raised when one or more errors occurred during model 

14 validation. 

15 """ 

16 

17 def __init__(self, errors, path=None): 

18 """ Initializer. 

19  

20 :param errors: List of Exception instances. Also accepts a string, 

21 which is converted to a TypeError. 

22 :param str path: The property path on the object where errors occurred 

23 """ 

24 if not isinstance(errors, list): 

25 errors = [TypeError(errors)] 

26 msgs = "\n ".join([str(e).replace("\n", "\n ") for e in errors]) 

27 message = "{}:\n {}".format(path or "{root}", msgs) 

28 

29 super(FHIRValidationError, self).__init__(message) 

30 

31 self.errors = errors 

32 """ A list of validation errors encountered. Typically contains 

33 TypeError, KeyError, possibly AttributeError and others. """ 

34 

35 self.path = path 

36 """ The path on the object where the errors occurred. """ 

37 

38 def prefixed(self, path_prefix): 

39 """ Creates a new instance of the receiver, with the given path prefix 

40 applied. """ 

41 path = '{}.{}'.format(path_prefix, self.path) if self.path is not None else path_prefix 

42 return self.__class__(self.errors, path) 

43 

44 

45class FHIRAbstractBase(object): 

46 """ Abstract base class for all FHIR elements. 

47 """ 

48 

49 def __init__(self, jsondict=None, strict=True): 

50 """ Initializer. If strict is true, raises on errors, otherwise uses 

51 `logger.warning()`. 

52  

53 :raises: FHIRValidationError on validation errors, unless strict is False 

54 :param dict jsondict: A JSON dictionary to use for initialization 

55 :param bool strict: If True (the default), invalid variables will raise a TypeError 

56 """ 

57 

58 self._resolved = None 

59 """ Dictionary of resolved resources. """ 

60 

61 self._owner = None 

62 """ Points to the parent resource, if there is one. """ 

63 

64 if jsondict is not None: 

65 if strict: 

66 self.update_with_json(jsondict) 

67 else: 

68 try: 

69 self.update_with_json(jsondict) 

70 except FHIRValidationError as e: 

71 for err in e.errors: 

72 logger.warning(err) 

73 

74 

75 # MARK: Instantiation from JSON 

76 

77 @classmethod 

78 def with_json(cls, jsonobj): 

79 """ Initialize an element from a JSON dictionary or array. 

80  

81 If the JSON dictionary has a "resourceType" entry and the specified 

82 resource type is not the receiving classes type, uses 

83 `FHIRElementFactory` to return a correct class instance. 

84  

85 :raises: TypeError on anything but dict or list of dicts 

86 :raises: FHIRValidationError if instantiation fails 

87 :param jsonobj: A dict or list of dicts to instantiate from 

88 :returns: An instance or a list of instances created from JSON data 

89 """ 

90 if isinstance(jsonobj, dict): 

91 return cls._with_json_dict(jsonobj) 

92 

93 if isinstance(jsonobj, list): 

94 arr = [] 

95 for jsondict in jsonobj: 

96 try: 

97 arr.append(cls._with_json_dict(jsondict)) 

98 except FHIRValidationError as e: 

99 raise e.prefixed(str(len(arr))) 

100 return arr 

101 

102 raise TypeError("`with_json()` on {} only takes dict or list of dict, but you provided {}" 

103 .format(cls, type(jsonobj))) 

104 

105 @classmethod 

106 def _with_json_dict(cls, jsondict): 

107 """ Internal method to instantiate from JSON dictionary. 

108  

109 :raises: TypeError on anything but dict 

110 :raises: FHIRValidationError if instantiation fails 

111 :returns: An instance created from dictionary data 

112 """ 

113 if not isinstance(jsondict, dict): 

114 raise TypeError("Can only use `_with_json_dict()` on {} with a dictionary, got {}" 

115 .format(type(self), type(jsondict))) 

116 return cls(jsondict) 

117 

118 @classmethod 

119 def with_json_and_owner(cls, jsonobj, owner): 

120 """ Instantiates by forwarding to `with_json()`, then remembers the 

121 "owner" of the instantiated elements. The "owner" is the resource 

122 containing the receiver and is used to resolve contained resources. 

123  

124 :raises: TypeError on anything but dict or list of dicts 

125 :raises: FHIRValidationError if instantiation fails 

126 :param dict jsonobj: Decoded JSON dictionary (or list thereof) 

127 :param FHIRElement owner: The owning parent 

128 :returns: An instance or a list of instances created from JSON data 

129 """ 

130 instance = cls.with_json(jsonobj) 

131 if isinstance(instance, list): 

132 for inst in instance: 

133 inst._owner = owner 

134 else: 

135 instance._owner = owner 

136 

137 return instance 

138 

139 

140 # MARK: (De)Serialization 

141 

142 def elementProperties(self): 

143 """ Returns a list of tuples, one tuple for each property that should 

144 be serialized, as: ("name", "json_name", type, is_list, "of_many", not_optional) 

145 """ 

146 return [] 

147 

148 def update_with_json(self, jsondict): 

149 """ Update the receiver with data in a JSON dictionary. 

150  

151 :raises: FHIRValidationError on validation errors 

152 :param dict jsondict: The JSON dictionary to use to update the receiver 

153 :returns: None on success, a list of errors if there were errors 

154 """ 

155 if jsondict is None: 

156 return 

157 

158 if not isinstance(jsondict, dict): 

159 raise FHIRValidationError("Non-dict type {} fed to `update_with_json` on {}" 

160 .format(type(jsondict), type(self))) 

161 

162 # loop all registered properties and instantiate 

163 errs = [] 

164 valid = set(['resourceType']) # used to also contain `fhir_comments` until STU-3 

165 found = set() 

166 nonoptionals = set() 

167 for name, jsname, typ, is_list, of_many, not_optional in self.elementProperties(): 

168 valid.add(jsname) 

169 if of_many is not None: 

170 valid.add(of_many) 

171 

172 # bring the value in shape 

173 err = None 

174 value = jsondict.get(jsname) 

175 if value is not None and hasattr(typ, 'with_json_and_owner'): 

176 try: 

177 value = typ.with_json_and_owner(value, self) 

178 except Exception as e: 

179 value = None 

180 err = e 

181 

182 # got a value, test if it is of required type and assign 

183 if value is not None: 

184 testval = value 

185 if is_list: 

186 if not isinstance(value, list): 

187 err = TypeError("Wrong type {} for list property \"{}\" on {}, expecting a list of {}" 

188 .format(type(value), name, type(self), typ)) 

189 testval = None 

190 else: 

191 testval = value[0] if value and len(value) > 0 else None 

192 

193 if testval is not None and not self._matches_type(testval, typ): 

194 err = TypeError("Wrong type {} for property \"{}\" on {}, expecting {}" 

195 .format(type(testval), name, type(self), typ)) 

196 else: 

197 setattr(self, name, value) 

198 

199 found.add(jsname) 

200 if of_many is not None: 

201 found.add(of_many) 

202 

203 # not optional and missing, report (we clean `of_many` later on) 

204 elif not_optional: 

205 nonoptionals.add(of_many or jsname) 

206 

207 # TODO: look at `_name` only if this is a primitive! 

208 _jsname = '_'+jsname 

209 _value = jsondict.get(_jsname) 

210 if _value is not None: 

211 valid.add(_jsname) 

212 found.add(_jsname) 

213 

214 # report errors 

215 if err is not None: 

216 errs.append(err.prefixed(name) if isinstance(err, FHIRValidationError) else FHIRValidationError([err], name)) 

217 

218 # were there missing non-optional entries? 

219 if len(nonoptionals) > 0: 

220 for miss in nonoptionals - found: 

221 errs.append(KeyError("Non-optional property \"{}\" on {} is missing" 

222 .format(miss, self))) 

223 

224 # were there superfluous dictionary keys? 

225 if len(set(jsondict.keys()) - valid) > 0: 

226 for supflu in set(jsondict.keys()) - valid: 

227 errs.append(AttributeError("Superfluous entry \"{}\" in data for {}" 

228 .format(supflu, self))) 

229 

230 if len(errs) > 0: 

231 raise FHIRValidationError(errs) 

232 

233 def as_json(self): 

234 """ Serializes to JSON by inspecting `elementProperties()` and creating 

235 a JSON dictionary of all registered properties. Checks: 

236  

237 - whether required properties are not None (and lists not empty) 

238 - whether not-None properties are of the correct type 

239  

240 :raises: FHIRValidationError if properties have the wrong type or if 

241 required properties are empty 

242 :returns: A validated dict object that can be JSON serialized 

243 """ 

244 js = {} 

245 errs = [] 

246 

247 # JSONify all registered properties 

248 found = set() 

249 nonoptionals = set() 

250 for name, jsname, typ, is_list, of_many, not_optional in self.elementProperties(): 

251 if not_optional: 

252 nonoptionals.add(of_many or jsname) 

253 

254 err = None 

255 value = getattr(self, name) 

256 if value is None: 

257 continue 

258 

259 if is_list: 

260 if not isinstance(value, list): 

261 err = TypeError("Expecting property \"{}\" on {} to be list, but is {}" 

262 .format(name, type(self), type(value))) 

263 elif len(value) > 0: 

264 if not self._matches_type(value[0], typ): 

265 err = TypeError("Expecting property \"{}\" on {} to be {}, but is {}" 

266 .format(name, type(self), typ, type(value[0]))) 

267 else: 

268 lst = [] 

269 for v in value: 

270 try: 

271 lst.append(v.as_json() if hasattr(v, 'as_json') else v) 

272 except FHIRValidationError as e: 

273 err = e.prefixed(str(len(lst))).prefixed(name) 

274 found.add(of_many or jsname) 

275 js[jsname] = lst 

276 else: 

277 if not self._matches_type(value, typ): 

278 err = TypeError("Expecting property \"{}\" on {} to be {}, but is {}" 

279 .format(name, type(self), typ, type(value))) 

280 else: 

281 try: 

282 found.add(of_many or jsname) 

283 js[jsname] = value.as_json() if hasattr(value, 'as_json') else value 

284 except FHIRValidationError as e: 

285 err = e.prefixed(name) 

286 

287 if err is not None: 

288 errs.append(err if isinstance(err, FHIRValidationError) else FHIRValidationError([err], name)) 

289 

290 # any missing non-optionals? 

291 if len(nonoptionals - found) > 0: 

292 for nonop in nonoptionals - found: 

293 errs.append(KeyError("Property \"{}\" on {} is not optional, you must provide a value for it" 

294 .format(nonop, self))) 

295 

296 if len(errs) > 0: 

297 raise FHIRValidationError(errs) 

298 return js 

299 

300 def _matches_type(self, value, typ): 

301 if value is None: 

302 return True 

303 if isinstance(value, typ): 

304 return True 

305 if int == typ or float == typ: 

306 return (isinstance(value, int) or isinstance(value, float)) 

307 if (sys.version_info < (3, 0)) and (str == typ or unicode == typ): 

308 return (isinstance(value, str) or isinstance(value, unicode)) 

309 return False 

310 

311 

312 # MARK: Handling References 

313 

314 def owningResource(self): 

315 """ Walks the owner hierarchy and returns the next parent that is a 

316 `DomainResource` instance. 

317 """ 

318 owner = self._owner 

319 while owner is not None and not hasattr(owner, "contained"): 

320 owner = owner._owner 

321 return owner 

322 

323 def owningBundle(self): 

324 """ Walks the owner hierarchy and returns the next parent that is a 

325 `Bundle` instance. 

326 """ 

327 owner = self._owner 

328 while owner is not None and not 'Bundle' == owner.resource_type: 

329 owner = owner._owner 

330 return owner 

331 

332 def resolvedReference(self, refid): 

333 """ Returns the resolved reference with the given id, if it has been 

334 resolved already. If it hasn't, forwards the call to its owner if it 

335 has one. 

336  

337 You should probably use `resolve()` on the `FHIRReference` itself. 

338  

339 :param refid: The id of the resource to resolve 

340 :returns: An instance of `Resource`, if it was found 

341 """ 

342 if self._resolved and refid in self._resolved: 

343 return self._resolved[refid] 

344 return self._owner.resolvedReference(refid) if self._owner is not None else None 

345 

346 def didResolveReference(self, refid, resolved): 

347 """ Called by `FHIRResource` when it resolves a reference. Stores the 

348 resolved reference into the `_resolved` dictionary. 

349  

350 :param refid: The id of the resource that was resolved 

351 :param refid: The resolved resource, ready to be cached 

352 """ 

353 if self._resolved is not None: 

354 self._resolved[refid] = resolved 

355 else: 

356 self._resolved = {refid: resolved} 

357