def test2(self): from fhirtordf.fhir.fhirmetavoc import FHIRMetaVocEntry m = FHIRMetaVocEntry(self.fhir_ontology, "Account") preds = m.predicates() v = [] for name in sorted(preds.keys()): pred = preds[name] t = m.predicate_type(pred) v.append((name, str(t), 'A' if m.is_atom(pred) else 'P' if m.is_primitive(t) else 'C')) self.assertEqual([ ('contained', 'http://hl7.org/fhir/Resource', 'C'), ('coverage', 'http://hl7.org/fhir/Account.CoverageComponent', 'C'), ('description', 'http://hl7.org/fhir/string', 'P'), ('extension', 'http://hl7.org/fhir/Extension', 'C'), ('guarantor', 'http://hl7.org/fhir/Account.GuarantorComponent', 'C'), ('id', 'http://hl7.org/fhir/id', 'P'), ('identifier', 'http://hl7.org/fhir/Identifier', 'C'), ('implicitRules', 'http://hl7.org/fhir/uri', 'P'), ('language', 'http://hl7.org/fhir/code', 'P'), ('meta', 'http://hl7.org/fhir/Meta', 'C'), ('modifierExtension', 'http://hl7.org/fhir/Extension', 'C'), ('name', 'http://hl7.org/fhir/string', 'P'), ('nodeRole', 'http://hl7.org/fhir/treeRoot', 'A'), ('owner', 'http://hl7.org/fhir/Reference', 'C'), ('partOf', 'http://hl7.org/fhir/Reference', 'C'), ('servicePeriod', 'http://hl7.org/fhir/Period', 'C'), ('status', 'http://hl7.org/fhir/code', 'P'), ('subject', 'http://hl7.org/fhir/Reference', 'C'), ('text', 'http://hl7.org/fhir/Narrative', 'C'), ('type', 'http://hl7.org/fhir/CodeableConcept', 'C') ], v)
def add_value_node(self, subj: Node, pred: URIRef, val: Union[JsonObj, str, List], valuetype: Optional[URIRef] = None) -> None: """ Expand val according to the range of pred and add it to the graph :param subj: graph subject :param pred: graph predicate :param val: JSON representation of target object :param valuetype: predicate type if it can't be directly determined """ pred_type = self._meta.predicate_type( pred) if not valuetype else valuetype # Transform generic resources into specific types if pred_type == FHIR.Resource: pred_type = FHIR[val.resourceType] val_meta = FHIRMetaVocEntry(self._vocabulary, pred_type) for k, p in val_meta.predicates().items(): if isinstance(val, JsonObj) and k in val: self.add_val(subj, p, val, k) if pred == FHIR.CodeableConcept.coding: self.add_type_arc(subj, val) elif k == "value" and val_meta.predicate_type(p) == FHIR.Element: # value / Element is the wild card combination -- if there is a "value[x]" in val, emit it where the # type comes from 'x' for vk in val._as_dict.keys(): if vk.startswith(k): self.add_val(subj, FHIR['Extension.' + vk], val, vk, self._meta.value_predicate_to_type(vk)) else: # Can have an extension only without a primary value self.add_extension_val(subj, val, k, p)
def test3(self): from fhirtordf.fhir.fhirmetavoc import FHIRMetaVocEntry m = FHIRMetaVocEntry(self.fhir_ontology, "Narrative") preds = m.predicates() v = [] for name in sorted(preds.keys()): pred = preds[name] t = m.predicate_type(pred) v.append((name, str(t), 'A' if m.is_atom(pred) else 'P' if m.is_primitive(t) else 'C')) self.assertEqual([ ('div', 'http://hl7.org/fhir/xhtml', 'A'), ('extension', 'http://hl7.org/fhir/Extension', 'C'), ('id', 'http://hl7.org/fhir/string', 'P'), ('index', 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger', 'A'), ('status', 'http://hl7.org/fhir/code', 'P')], v)
class FHIRResource: """ A FHIR RDF representation of a FHIR JSON resource """ def __init__(self, vocabulary: Graph, json_fname: Optional[str], base_uri: str, data: Optional[JsonObj] = None, target: Optional[Graph] = None, add_ontology_header: bool = True, replace_narrative_text: bool = False, is_root=True, resource_uri: Optional[URIRef] = None): """ Construct an RDF representation :param vocabulary: FHIR Metadata Vocabulary (fhir.ttl) :param json_fname: URI or file name of resource to convert :param base_uri: base of resource URI -- will be combined with the resource id to generate the actual URI :param data: if present load this data rather than json_fname :param target: target graph -- used for collections, bundles, etc. :param add_ontology_header: Add the OWL ontology header to the output :param replace_narrative_text: Replace long narrative text section with boilerplate :param is_root: True means this is a root node, False a component :param resource_uri: If present, this becomes the resource subject """ if json_fname: self.root = load(json_fname) elif data: self.root = data else: assert False, "Either a json file name or actual data image must be supplied" self._base_uri = base_uri + ('/' if base_uri[-1] not in '/#' else '') if 'resourceType' not in self.root: raise ValueError("{} is not a FHIR resource".format(json_fname)) if resource_uri: self._resource_uri = resource_uri else: if 'id' not in self.root: self.root.id = str(uuid4()) self._resource_uri = URIRef(self._base_uri + self.root.resourceType + '/' + self.root.id) self._meta = FHIRMetaVocEntry(vocabulary, FHIR[self.root.resourceType]) self._g = PrettyGraph() if target is None else target self._vocabulary = vocabulary self._addl_namespaces = dict() self._add_ontology_header = add_ontology_header self._replace_narrative_text = replace_narrative_text self.generate(is_root) @property def resource_id(self) -> Optional[str]: return value(self._g, self._resource_uri, FHIR.Resource.id) @property def resource_type(self) -> str: return self.root.resourceType @property def graph(self): return self._g def add_prefixes(self, nsmap: Dict[str, Namespace]) -> None: """ Add the required prefix definitions :return: """ [self._g.bind(e[0], e[1]) for e in nsmap.items()] def add_ontology_definition(self) -> None: ont_uri = URIRef(str(self._resource_uri) + ".ttl") self.add(ont_uri, RDF.type, OWL.Ontology)\ .add(ont_uri, OWL.imports, FHIR['fhir.ttl']) if 'meta' in self.root and 'versionId' in self.root.meta: ont_uri_str = str(ont_uri) if re.search(r'\.\w+$', ont_uri_str): ont_uri_str, suffix = ont_uri_str.rsplit('.', 1) suffix = '.' + suffix else: suffix = '' self.add( ont_uri, OWL.versionIRI, URIRef(ont_uri_str + '/_history/' + self.root.meta.versionId + suffix)) def add(self, subj: Node, pred: URIRef, obj: Node) -> "FHIRResource": """ Shortcut to rdflib add function :param subj: :param pred: :param obj: :return: self for chaining """ self._g.add((subj, pred, obj)) return self def add_value_node(self, subj: Node, pred: URIRef, val: Union[JsonObj, str, List], valuetype: Optional[URIRef] = None) -> None: """ Expand val according to the range of pred and add it to the graph :param subj: graph subject :param pred: graph predicate :param val: JSON representation of target object :param valuetype: predicate type if it can't be directly determined """ pred_type = self._meta.predicate_type( pred) if not valuetype else valuetype # Transform generic resources into specific types if pred_type == FHIR.Resource: pred_type = FHIR[val.resourceType] val_meta = FHIRMetaVocEntry(self._vocabulary, pred_type) for k, p in val_meta.predicates().items(): if isinstance(val, JsonObj) and k in val: self.add_val(subj, p, val, k) if pred == FHIR.CodeableConcept.coding: self.add_type_arc(subj, val) elif k == "value" and val_meta.predicate_type(p) == FHIR.Element: # value / Element is the wild card combination -- if there is a "value[x]" in val, emit it where the # type comes from 'x' for vk in val._as_dict.keys(): if vk.startswith(k): self.add_val(subj, FHIR['Extension.' + vk], val, vk, self._meta.value_predicate_to_type(vk)) else: # Can have an extension only without a primary value self.add_extension_val(subj, val, k, p) def add_reference(self, subj: Node, val: str) -> None: """ Add a fhir:link and RDF type arc if it can be determined :param subj: reference subject :param val: reference value """ match = FHIR_RESOURCE_RE.match(val) ref_uri_str = res_type = None if match: ref_uri_str = val if match.group(FHIR_RE_BASE) else ( self._base_uri + urllib.parse.quote(val)) res_type = match.group(FHIR_RE_RESOURCE) elif '://' in val: ref_uri_str = val res_type = "Resource" elif self._base_uri and not val.startswith('#') and not val.startswith( '/'): ref_uri_str = self._base_uri + urllib.parse.quote(val) res_type = val.split('/', 1)[0] if '/' in val else "Resource" if ref_uri_str: ref_uri = URIRef(ref_uri_str) self.add(subj, FHIR.link, ref_uri) self.add(ref_uri, RDF.type, FHIR[res_type]) def add_type_arc(self, subj: Node, val: JsonObj) -> None: if "system" in val and "code" in val: for k in codesystem_maps.keys(): if (isinstance(k, str) and k == val.system) or (not isinstance(k, str) and k.match(val.system)): type_uri = codesystem_maps[k](val.system, urllib.parse.quote(val.code), self._addl_namespaces) if type_uri: self.add(subj, RDF.type, type_uri) break def node_subject(self, list_idx: int, subj: Node, pred: URIRef, node: JsonObj) -> Node: if pred == FHIR.Bundle.entry: entry = BNode() self.add(entry, FHIR.index, Literal(list_idx)) self.add_val(entry, FHIR.Bundle.entry.fullUrl, node, 'fullUrl') self.add(entry, FHIR.Bundle.entry.resource, URIRef(node.fullUrl)) self.add(subj, pred, entry) return URIRef(node.fullUrl) else: return BNode() def add_val(self, subj: Node, pred: URIRef, json_obj: JsonObj, json_key: str, valuetype: Optional[URIRef] = None) -> Optional[BNode]: """ Add the RDF representation of val to the graph as a target of subj, pred. Note that FHIR lists are represented as a list of BNODE objects with a fhir:index discrimanant :param subj: graph subject :param pred: predicate :param json_obj: object containing json_key :param json_key: name of the value in the JSON resource :param valuetype: value type if NOT determinable by predicate :return: value node if target is a BNode else None """ if json_key not in json_obj: print("Expecting to find object named '{}' in JSON:".format( json_key)) print(json_obj._as_json_dumps()) print("entry skipped") return None val = json_obj[json_key] if isinstance(val, List): list_idx = 0 for lv in val: entry_bnode = BNode() # TODO: this is getting messy. Refactor and clean this up if pred == FHIR.Bundle.entry: entry_subj = URIRef(lv.fullUrl) self.add(entry_bnode, FHIR.index, Literal(list_idx)) self.add_val(entry_bnode, FHIR.Bundle.entry.fullUrl, lv, 'fullUrl') self.add(entry_bnode, FHIR.Bundle.entry.resource, entry_subj) self.add(subj, pred, entry_bnode) entry_mv = FHIRMetaVocEntry(self._vocabulary, FHIR.BundleEntryComponent) for k, p in entry_mv.predicates().items(): if k not in ['resource', 'fullUrl'] and k in lv: print("---> adding {}".format(k)) self.add_val(subj, p, lv, k) FHIRResource(self._vocabulary, None, self._base_uri, lv.resource, self._g, False, self._replace_narrative_text, False, resource_uri=entry_subj) else: self.add(entry_bnode, FHIR.index, Literal(list_idx)) if isinstance(lv, JsonObj): self.add_value_node(entry_bnode, pred, lv, valuetype) else: vt = self._meta.predicate_type(pred) atom_type = self._meta.primitive_datatype_nostring( vt) if vt else None self.add(entry_bnode, FHIR.value, Literal(lv, datatype=atom_type)) self.add(subj, pred, entry_bnode) list_idx += 1 else: vt = self._meta.predicate_type( pred) if not valuetype else valuetype if self._meta.is_atom(pred): if self._replace_narrative_text and pred == FHIR.Narrative.div and len( val) > 120: val = REPLACED_NARRATIVE_TEXT self.add(subj, pred, Literal(val)) else: v = BNode() if self._meta.is_primitive(vt): self.add( v, FHIR.value, Literal( str(val), datatype=self._meta.primitive_datatype_nostring( vt, val))) else: self.add_value_node(v, pred, val, valuetype) self.add(subj, pred, v) if pred == FHIR.Reference.reference: self.add_reference(subj, val) elif pred == FHIR.RelatedArtifact.resource: self.add_reference(v, val) self.add_extension_val(v, json_obj, json_key) return v return None def add_extension_val(self, subj: Node, json_obj: Union[JsonObj, List[JsonObjTypes]], key: str, pred: Optional[URIRef] = None) -> None: """ Add any extensions for the supplied object. This can be called in following situations: 1) Single extended value "key" : (value), "_key" : { "extension": [ { "url": "http://...", "value[x]": "......" } ] } 2) Single extension only "_key" : { "extension": [ { "url": "http://...", "value[x]": "......" } ] } 3) Multiple extended values: (TBD) 4) Multiple extensions only "_key" : [ { "extension": [ { "url": "http://...", "value[x]": "......" } ] } ] :param subj: Node containing subject :param json_obj: Object (potentially) containing "_key" :param key: name of element that is possibly extended (as indicated by "_" prefix) :param pred: predicate for the contained elements. Only used in situations 3) (?) and 4 """ extendee_name = "_" + key if extendee_name in json_obj: if not isinstance(subj, BNode): raise NotImplementedError( "Extension to something other than a simple BNode") if isinstance(json_obj[extendee_name], list): if not pred: raise NotImplemented("Case 3 not implemented") entry_idx = 0 for extension in json_obj[extendee_name]: entry = BNode() self.add(entry, FHIR.index, Literal(entry_idx)) self.add_val(entry, FHIR.Element.extension, extension, 'extension') self.add(subj, pred, entry) entry_idx += 1 elif 'fhir_comments' in json_obj[extendee_name] and len( json_obj[extendee_name]) == 1: # TODO: determine whether and how fhir comments should be represented in RDF. # for the moment we just drop them print("fhir_comment ignored") print(json_obj[extendee_name]._as_json_dumps()) pass else: self.add_val(subj, FHIR.Element.extension, json_obj[extendee_name], 'extension') def add_resource(self, subj: URIRef, json_obj: JsonObj): self.add(subj, RDF.type, FHIR[json_obj.resourceType]) for k, p in self._meta.predicates().items(): if k in json_obj: self.add_val(subj, p, json_obj, k) def generate(self, is_root: bool) -> Graph: if is_root: self.add_prefixes(namespaces) if self._add_ontology_header: self.add_ontology_definition() self.add(self._resource_uri, FHIR.nodeRole, FHIR.treeRoot) self.add_resource(self._resource_uri, self.root) self.add_prefixes(self._addl_namespaces) return self._g def __str__(self): return self._g.serialize()