Coverage for /home/martinb/workspace/client-py/fhirclient/models/fhirabstractbase.py : 61%

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.
6import sys
7import logging
9logger = logging.getLogger(__name__)
12class FHIRValidationError(Exception):
13 """ Exception raised when one or more errors occurred during model
14 validation.
15 """
17 def __init__(self, errors, path=None):
18 """ Initializer.
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)
29 super(FHIRValidationError, self).__init__(message)
31 self.errors = errors
32 """ A list of validation errors encountered. Typically contains
33 TypeError, KeyError, possibly AttributeError and others. """
35 self.path = path
36 """ The path on the object where the errors occurred. """
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)
45class FHIRAbstractBase(object):
46 """ Abstract base class for all FHIR elements.
47 """
49 def __init__(self, jsondict=None, strict=True):
50 """ Initializer. If strict is true, raises on errors, otherwise uses
51 `logger.warning()`.
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 """
58 self._resolved = None
59 """ Dictionary of resolved resources. """
61 self._owner = None
62 """ Points to the parent resource, if there is one. """
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)
75 # MARK: Instantiation from JSON
77 @classmethod
78 def with_json(cls, jsonobj):
79 """ Initialize an element from a JSON dictionary or array.
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.
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)
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
102 raise TypeError("`with_json()` on {} only takes dict or list of dict, but you provided {}"
103 .format(cls, type(jsonobj)))
105 @classmethod
106 def _with_json_dict(cls, jsondict):
107 """ Internal method to instantiate from JSON dictionary.
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)
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.
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
137 return instance
140 # MARK: (De)Serialization
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 []
148 def update_with_json(self, jsondict):
149 """ Update the receiver with data in a JSON dictionary.
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
158 if not isinstance(jsondict, dict):
159 raise FHIRValidationError("Non-dict type {} fed to `update_with_json` on {}"
160 .format(type(jsondict), type(self)))
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)
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
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
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)
199 found.add(jsname)
200 if of_many is not None:
201 found.add(of_many)
203 # not optional and missing, report (we clean `of_many` later on)
204 elif not_optional:
205 nonoptionals.add(of_many or jsname)
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)
214 # report errors
215 if err is not None:
216 errs.append(err.prefixed(name) if isinstance(err, FHIRValidationError) else FHIRValidationError([err], name))
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)))
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)))
230 if len(errs) > 0:
231 raise FHIRValidationError(errs)
233 def as_json(self):
234 """ Serializes to JSON by inspecting `elementProperties()` and creating
235 a JSON dictionary of all registered properties. Checks:
237 - whether required properties are not None (and lists not empty)
238 - whether not-None properties are of the correct type
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 = []
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)
254 err = None
255 value = getattr(self, name)
256 if value is None:
257 continue
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)
287 if err is not None:
288 errs.append(err if isinstance(err, FHIRValidationError) else FHIRValidationError([err], name))
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)))
296 if len(errs) > 0:
297 raise FHIRValidationError(errs)
298 return js
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
312 # MARK: Handling References
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
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
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.
337 You should probably use `resolve()` on the `FHIRReference` itself.
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
346 def didResolveReference(self, refid, resolved):
347 """ Called by `FHIRResource` when it resolves a reference. Stores the
348 resolved reference into the `_resolved` dictionary.
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}