def test_csolink_model(self): """ Make sure the csolink model is valid """ schema = SchemaLoader(CSOLINK_MODEL_YAML) errors = [] try: schema.resolve() except ValueError as e: errors.append(str(e)) if not errors: errors = schema.synopsis.errors() self.assertEqual([], errors, "csolink-model.yaml - errors detected")
def cli(inputs, view: bool): """ Generates SQL VIEW commands from hints embedded in linkml """ for input in inputs: with open(input, 'r') as stream: schema = load_raw_schema(input) print('-- ** REWRITE TABLES AS VIEWS **') print(f'-- SCHEMA: {schema.id}') loader = SchemaLoader(schema, mergeimports=True) loader.resolve() generate_views_from_linkml(schema, view)
def eval_synopsis(self, base_name: str, source: Optional[str] = None) -> None: schema = SchemaLoader(source if source else env.input_path(base_name + '.yaml'), importmap=env.import_map) schema.resolve() self.summary = schema.synopsis.summary() self.env.generate_single_file( base_name + '.errs', lambda: '\n'.join(schema.synopsis.errors()), value_is_returned=True) self.env.generate_single_file(base_name + '.synopsis', lambda: self.summary, value_is_returned=True)
def cli(inputs, limit: int): """ Generates SQL VIEW commands from hints embedded in linkml linkml """ for input in inputs: with open(input, 'r') as stream: schema = load_raw_schema(input) print('-- ** REWRITE TABLES AS VIEWS **') print(f'-- SCHEMA: {schema.id}') loader = SchemaLoader(schema, mergeimports=True) loader.resolve() for cn, c in schema.classes.items(): if c.mixin: continue if len(c.slots) > 0: if not c.abstract and not c.mixin: sql_table = underscore(cn) print(f'SELECT * FROM {sql_table} LIMIT {limit};') else: print(f'-- No slots for {cn}')
def eval_loader(self, base_name: str, logger: Optional[logging.Logger] = None, source: Optional[str] = None) -> None: loader = SchemaLoader(source or self.env.input_path(base_name + '.yaml'), logger=logger) self.env.generate_single_file(base_name + '.json', lambda: as_json(loader.resolve()), filtr=json_metadata_filter, value_is_returned=True) self.env.generate_single_file( base_name + '.errs', lambda: '\n'.join(loader.synopsis.errors()), filtr=json_metadata_filter, value_is_returned=True)
class OwlSchemaGenerator(Generator): generatorname = os.path.basename(__file__) generatorversion = "0.1.1" valid_formats = ['owl', 'ttl'] + [ x.name for x in rdflib_plugins(None, rdflib_Parser) if '/' not in str(x.name) ] visits_are_sorted = True def __init__(self, schema: Union[str, TextIO, SchemaDefinition], **kwargs) -> None: super().__init__(schema, **kwargs) self.graph: Optional[Graph] = None self.metamodel = SchemaLoader(LOCAL_METAMODEL_YAML_FILE, importmap=kwargs.get('importmap', None), mergeimports=self.merge_imports) \ if os.path.exists(LOCAL_METAMODEL_YAML_FILE) else\ SchemaLoader(METAMODEL_YAML_URI, base_dir=META_BASE_URI, importmap=kwargs.get('importmap', None), mergeimports=self.merge_imports) self.metamodel.resolve() self.top_value_uri: Optional[URIRef] = None def visit_schema(self, output: Optional[str] = None, **_): base = URIRef(self.schema.id) self.graph = Graph(identifier=base) for prefix in self.metamodel.schema.emit_prefixes: self.graph.bind(prefix, self.metamodel.namespaces[prefix]) self.graph.add((base, RDF.type, OWL.Ontology)) self._add_element_properties(base, self.schema) # add the model types for name in [ 'class_definition', 'type_definition', 'slot_definition', 'subset_definition' ]: self._add_metamodel_class(name) # add value placeholder self.top_value_uri = self.metamodel.namespaces[ METAMODEL_NAMESPACE_NAME]['topValue'] self.graph.add((self.top_value_uri, RDF.type, OWL.DatatypeProperty)) self.graph.add((self.top_value_uri, RDFS.label, Literal("value"))) def end_schema(self, output: Optional[str] = None, **_) -> None: data = self.graph.serialize(format='turtle' if self.format in ['owl', 'ttl'] else self.format).decode() if output: with open(output, 'w') as outf: outf.write(data) else: print(data) def add_metadata(self, e: SchemaDefinition, uri: URIRef) -> None: if e.aliases is not None: for s in e.aliases: self.graph.add((uri, SKOS.altLabel, Literal(s))) if e.mappings is not None: for m in e.mappings: m_uri = self.namespaces.uri_for(m) if m_uri is not None: self.graph.add((uri, SKOS.exactMatch, m_uri)) else: logging.warning(f'No URI for {m}') if e.exact_mappings is not None: for m in e.exact_mappings: m_uri = self.namespaces.uri_for(m) if m_uri is not None: self.graph.add((uri, SKOS.exactMatch, m_uri)) else: logging.warning(f'No URI for {m}') if e.close_mappings is not None: for m in e.close_mappings: m_uri = self.namespaces.uri_for(m) if m_uri is not None: self.graph.add((uri, SKOS.closeMatch, m_uri)) else: logging.warning(f'No URI for {m}') if e.narrow_mappings is not None: for m in e.narrow_mappings: m_uri = self.namespaces.uri_for(m) if m_uri is not None: self.graph.add((uri, SKOS.narrowMatch, m_uri)) else: logging.warning(f'No URI for {m}') if e.broad_mappings is not None: for m in e.broad_mappings: m_uri = self.namespaces.uri_for(m) if m_uri is not None: self.graph.add((uri, SKOS.broadMatch, m_uri)) else: logging.warning(f'No URI for {m}') if e.related_mappings is not None: for m in e.related_mappings: m_uri = self.namespaces.uri_for(m) if m_uri is not None: self.graph.add((uri, SKOS.relatedMatch, m_uri)) else: logging.warning(f'No URI for {m}') def visit_class(self, cls: ClassDefinition) -> bool: cls_uri = self._class_uri(cls.name) self.add_metadata(cls, cls_uri) self.graph.add((cls_uri, RDF.type, OWL.Class)) self.graph.add((cls_uri, RDF.type, self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][ camelcase('class definition')])) self._add_element_properties(cls_uri, cls) # Parent classes # TODO: reintroduce this # if not cls.defining_slots: if True: if cls.is_a: self.graph.add( (cls_uri, RDFS.subClassOf, self._class_uri(cls.is_a))) if cls.mixin: self.graph.add( (cls_uri, RDFS.subClassOf, METAMODEL_NAMESPACE.mixin)) for mixin in sorted(cls.mixins): self.graph.add( (cls_uri, RDFS.subClassOf, self._class_uri(mixin))) if cls.name in self.synopsis.applytorefs: for appl in sorted( self.synopsis.applytorefs[cls.name].classrefs): self.graph.add( (cls_uri, RDFS.subClassOf, self._class_uri(appl))) else: raise NotImplementedError("Defining slots need to be implemented") # If defining slots, we generate an equivalentClass entry # equ_node = BNode() # self.graph.add((cls_uri, OWL.equivalentClass, equ_node)) # self.graph.add((equ_node, RDF.type, OWL.Class)) # # elts = [] # if cls.is_a: # elts.append(self._class_uri(cls.is_a)) # if cls.mixin: # self.graph.add((cls_uri, RDFS.subClassOf, META_NS.mixin)) # for mixin in cls.mixins: # self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(mixin))) # if cls.name in self.synopsis.applytorefs: # for appl in self.synopsis.applytorefs[cls.name].classrefs: # self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(appl))) # # for slotname in cls.defining_slots: # restr_node = BNode() # slot = self.schema.slots[slotname] # # self.graph.add((restr_node, RDF.type, OWL.Restriction)) # self.graph.add((restr_node, OWL.onProperty, self._prop_uri(slotname))) # self._add_cardinality(restr_node, slot) # # TODO: fix this # # self.graph.add((restr_node, OWL.someValuesFrom, self._build_range(slot))) # elts.append(restr_node) # # coll_bnode = BNode() # Collection(self.graph, coll_bnode, elts) # self.graph.add((equ_node, OWL.intersectionOf, coll_bnode)) # TODO: see whether unions belong # if cls.union_of: # union_node = BNode() # Collection(self.graph, union_coll, [self.class_uri(union_node) for union_node in cls.union_of]) # self.graph.add((union_node, OWL.unionOf, union_coll)) # self.graph.add((cls_uri, RDFS.subClassOf, union_node)) for sn in sorted(self.own_slot_names(cls)): # Defining_slots are covered above if sn not in cls.defining_slots: slot = self.schema.slots[sn] # Non-inherited slots are annotation properties if self.is_slot_object_property(slot): slot_node = BNode() self.graph.add((cls_uri, RDFS.subClassOf, slot_node)) # required multivalued if slot.required: if slot.multivalued: # y y intersectionOf(restriction(slot only type) restriction(slot some type) restr1 = BNode() self.graph.add((restr1, RDF.type, OWL.Restriction)) self.graph.add((restr1, OWL.allValuesFrom, self._range_uri(slot))) self.graph.add( (restr1, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot)))) restr2 = BNode() self.graph.add((restr2, RDF.type, OWL.Restriction)) self.graph.add((restr2, OWL.someValuesFrom, self._range_uri(slot))) self.graph.add( (restr2, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot)))) coll_bnode = BNode() Collection(self.graph, coll_bnode, [restr1, restr2]) self.graph.add( (slot_node, OWL.intersectionOf, coll_bnode)) self.graph.add((slot_node, RDF.type, OWL.Class)) else: # y n restriction(slot exactly 1 type) self.graph.add( (slot_node, RDF.type, OWL.Restriction)) self.graph.add( (slot_node, OWL.qualifiedCardinality, Literal(1))) self.graph.add( (slot_node, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot)))) self.graph.add((slot_node, OWL.onClass, self._range_uri(slot))) else: if slot.multivalued: # n y restriction(slot only type) self.graph.add( (slot_node, RDF.type, OWL.Restriction)) self.graph.add((slot_node, OWL.allValuesFrom, self._range_uri(slot))) self.graph.add( (slot_node, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot)))) else: # n n intersectionOf(restriction(slot only type) restriction(slot max 1 type)) self.graph.add( (slot_node, RDF.type, OWL.Restriction)) self.graph.add((slot_node, OWL.onClass, self._range_uri(slot))) self.graph.add( (slot_node, OWL.maxQualifiedCardinality, Literal(1))) self.graph.add( (slot_node, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot)))) return True def visit_slot(self, slot_name: str, slot: SlotDefinition) -> None: """ Add a slot definition per slot @param slot_name: @param slot: @return: """ # Note: We use the raw name in OWL and add a subProperty arc slot_uri = self._prop_uri(slot.name) self._add_element_properties(slot_uri, slot) # Inherited slots are object or data properties # All others are annotation properties self.graph.add( (slot_uri, RDF.type, OWL.ObjectProperty if self.is_slot_object_property(slot) else OWL.AnnotationProperty)) self.graph.add((slot_uri, RDF.type, self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][ camelcase('slot definition')])) self.graph.add((slot_uri, RDFS.range, self._range_uri(slot))) if slot.domain: self.graph.add( (slot_uri, RDFS.domain, self._class_uri(slot.domain))) if slot.inverse: self.graph.add( (slot_uri, OWL.inverseOf, self._prop_uri(slot.inverse))) if slot.symmetric: self.graph.add((slot_uri, RDF.type, OWL.SymmetricProperty)) # Parent slots if slot.is_a: self.graph.add( (slot_uri, RDFS.subPropertyOf, self._prop_uri(slot.is_a))) for mixin in slot.mixins: self.graph.add( (slot_uri, RDFS.subPropertyOf, self._prop_uri(mixin))) if slot.name in self.synopsis.applytorefs: for appl in self.synopsis.applytorefs[slot.name].slotrefs: self.graph.add( (slot_uri, RDFS.subClassOf, self._prop_uri(appl))) def visit_type(self, typ: TypeDefinition) -> None: type_uri = self._type_uri(typ.name) self.graph.add((type_uri, RDF.type, OWL.Class)) self.graph.add((type_uri, RDF.type, self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][ camelcase('type definition')])) self._add_element_properties(type_uri, typ) if typ.typeof: self.graph.add( (type_uri, RDFS.subClassOf, self._type_uri(typ.typeof))) else: restr = BNode() self.graph.add((restr, RDF.type, OWL.Restriction)) self.graph.add((restr, OWL.qualifiedCardinality, Literal(1))) self.graph.add((restr, OWL.onProperty, self.top_value_uri)) self.graph.add( (restr, OWL.onDataRange, self.namespaces.uri_for(typ.uri))) self.graph.add((type_uri, RDFS.subClassOf, restr)) def _add_element_properties(self, uri: URIRef, el: Element) -> None: for k, v in el.__dict__.items(): if k in self.metamodel.schema.slots: defining_slot = self.metamodel.schema.slots[k] if v is not None and 'owl' in defining_slot.in_subset: ve = v if isinstance(v, list) else [v] for e in ve: self.graph.add( (uri, URIRef( self.metamodel.namespaces.uri_for( defining_slot.slot_uri)), Literal(e))) def _add_cardinality(self, subj: Union[BNode, URIRef], slot) -> None: """ Add cardinality restrictions to node """ if slot.required: if slot.multivalued: self.graph.add((subj, OWL.minCardinality, Literal(1))) else: self.graph.add((subj, OWL.cardinality, Literal(1))) elif not slot.multivalued: self.graph.add((subj, OWL.maxCardinality, Literal(1))) def _range_uri(self, slot: SlotDefinition) -> URIRef: if slot.range in self.schema.types: return self._type_uri(TypeDefinitionName(slot.range)) elif slot.range in self.schema.enums: # TODO: enums fill this in return self._enum_uri(EnumDefinitionName(slot.range)) else: return self._class_uri(ClassDefinitionName(slot.range)) def _class_uri(self, cn: ClassDefinitionName) -> URIRef: c = self.schema.classes[cn] return URIRef(c.definition_uri) def _enum_uri(self, en: EnumDefinitionName) -> URIRef: # TODO: enums e = self.schema.enums[en] return URIRef(f"http://UNKNOWN.org/{en}") def _prop_uri(self, pn: SlotDefinitionName) -> URIRef: p = self.schema.slots.get(pn, None) if p is None or p.definition_uri is None: return self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][ underscore(pn)] else: return URIRef(p.definition_uri) def _type_uri(self, tn: TypeDefinitionName) -> URIRef: t = self.schema.types[tn] return URIRef(t.definition_uri) def _add_metamodel_class(self, cname: str) -> None: metac = self.metamodel.schema.classes[cname] metac_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][ camelcase(metac.name)] self.graph.add((metac_uri, RDF.type, OWL.Class)) self._add_element_properties(metac_uri, metac) def is_slot_object_property(self, slot: SlotDefinition) -> bool: return True
def __init__(self, schema: Union[str, TextIO, SchemaDefinition, "Generator"], format: Optional[str] = None, emit_metadata: bool = True, useuris: Optional[bool] = None, importmap: Optional[str] = None, log_level: int = DEFAULT_LOG_LEVEL_INT, mergeimports: Optional[bool] = True, source_file_date: Optional[str] = None, source_file_size: Optional[int] = None, logger: Optional[logging.Logger] = None, **kwargs) -> None: """ Constructor :param schema: metamodel compliant schema. Can be URI, file name, actual schema, another generator, an open file or a pre-parsed schema. :param format: expected output format :param emit_metadata: True means include date, generator, etc. information in source header if appropriate :param useuris: True means declared class slot uri's are used. False means use model uris :param importmap: File name of import mapping file -- maps import name/uri to target :param log_level: Logging level :param mergeimports: True means merge non-linkml sources into importing package. False means separate packages. :param source_file_date: Modification date of input source file :param source_file_size: Source file size :param logger: pre-set logger """ if logger: self.logger = logger else: logging.basicConfig() self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(log_level) if format is None: format = self.valid_formats[0] assert format in self.valid_formats, f"Unrecognized format: {format}" self.format = format self.emit_metadata = emit_metadata self.merge_imports = mergeimports self.source_file_date = source_file_date if emit_metadata else None self.source_file_size = source_file_size if emit_metadata else None if isinstance(schema, Generator): gen = schema self.schema = gen.schema self.synopsis = gen.synopsis self.loaded = gen.loaded self.namespaces = gen.namespaces self.base_dir = gen.base_dir self.importmap = gen.importmap self.source_file_data = gen.source_file_date self.source_file_size = gen.source_file_size self.schema_location = gen.schema_location self.schema_defaults = gen.schema_defaults self.logger = gen.logger else: loader = SchemaLoader(schema, self.base_dir, useuris=useuris, importmap=importmap, logger=self.logger, mergeimports=mergeimports, emit_metadata=emit_metadata, source_file_date=self.source_file_date, source_file_size=self.source_file_size) loader.resolve() self.schema = loader.schema self.synopsis = loader.synopsis self.loaded = loader.loaded self.namespaces = loader.namespaces self.base_dir = loader.base_dir self.importmap = loader.importmap self.source_file_data = loader.source_file_date self.source_file_size = loader.source_file_size self.schema_location = loader.schema_location self.schema_defaults = loader.schema_defaults