def _visit(self, node: Any) -> Optional[Any]: if isinstance(node, (YAMLRoot, dict)) or is_YAMLROOT(node): if isinstance(node, YAMLRoot) or is_YAMLROOT(node): node = self._add_type(node) for k, v in list(node.items()): if v: new_v = self._visit(v) if new_v is not None: node[k] = new_v elif isinstance(node, list): for i in range(0, len(node)): new_v = self._visit(node[i]) if new_v is not None: node[i] = new_v elif isinstance(node, set): for v in list(node): new_v = self._visit(v) if new_v is not None: node.remove(v) node.add(new_v) elif isinstance(node, ClassDefinitionName): return ClassDefinitionName(camelcase(node)) elif isinstance(node, SlotDefinitionName): return SlotDefinitionName(underscore(node)) elif isinstance(node, TypeDefinitionName): return TypeDefinitionName(underscore(node)) elif isinstance(node, SubsetDefinitionName): return SubsetDefinitionName(underscore(node)) elif isinstance(node, ElementName): return ClassDefinitionName(camelcase(node)) if node in self.schema.classes else \ SlotDefinitionName(underscore(node)) if node in self.schema.slots else \ SubsetDefinitionName(camelcase(node)) if node in self.schema.subsets else \ TypeDefinitionName(underscore(node)) if node in self.schema.types else None return None
def _link(self, obj: Optional[Element], *, after_link: str = None, use_desc: bool=False, add_subset: bool=True) -> str: """ Create a link to ref if appropriate. @param ref: the name or value of a class, slot, type or the name of a built in type. @param after_link: Text to put between link and description @param use_desc: True means append a description after the link if available @param add_subset: True means add any subset information that is available @return: """ nl = '\n' if obj is None or not self.is_secondary_ref(obj.name): return self.bbin(obj) if isinstance(obj, SlotDefinition): link_name = ((be(obj.domain) + '➞') if obj.alias else '') + self.aliased_slot_name(obj) link_ref = underscore(obj.name) elif isinstance(obj, TypeDefinition): link_name = camelcase(obj.name) link_ref = f"types/{link_name}" if not self.no_types_dir else f"{link_name}" elif isinstance(obj, ClassDefinition): link_name = camelcase(obj.name) link_ref = camelcase(link_name) else: link_name = obj.name link_ref = link_name desc = self.desc_for(obj, use_desc) return f'[{link_name}]' \ f'({link_ref}.{self.format})' + \ (f' {after_link} ' if after_link else '') + (f' - {desc.split(nl)[0]}' if desc else '')
def gen_references(self) -> str: """ Generate python type declarations for all identifiers (primary keys) """ rval = [] for cls in self._sort_classes(self.schema.classes.values()): if not cls.imported_from: pkeys = self.primary_keys_for(cls) if pkeys: for pk in pkeys: classname = camelcase(cls.name) + camelcase( self.aliased_slot_name(pk)) # If we've got a parent slot and the range of the parent is the range of the child, the # child slot is a subclass of the parent. Otherwise, the child range has been overridden, # so the inheritence chain has been broken parent_pk = self.class_identifier( cls.is_a) if cls.is_a else None parent_pk_slot = self.schema.slots[ parent_pk] if parent_pk else None pk_slot = self.schema.slots[pk] if parent_pk_slot and (parent_pk_slot.name == pk or pk_slot.range == parent_pk_slot.range): parents = self.class_identifier_path( cls.is_a, False) else: parents = self.slot_range_path(pk_slot) parent_cls = 'extended_' + parents[-1] if parents[ -1] in ['str', 'float', 'int'] else parents[-1] rval.append( f'class {classname}({parent_cls}):\n\tpass') break # We only do the first primary key return '\n\n\n'.join(rval)
def end_class(self, cls: ClassDefinition) -> None: tname = camelcase(cls.name) if self.use_inherits and cls.is_a: # postgresql supports inheritance # if you want to use plain SQL DDL then use sqlutils to unfold hierarchy # TODO: raise error if the target is standard SQL p = camelcase(cls.is_a) logging.error("Not supported in sqlalchemy")
def test_formats(self): self.assertEqual("ThisIsIt", camelcase("this is it")) self.assertEqual("ThisIsIT", camelcase(" this is iT ")) self.assertEqual("IBeY", camelcase("i be y ")) self.assertEqual("ThisIsIt", camelcase("This__is_it")) self.assertEqual("this_is_it", underscore(" this is it ")) self.assertEqual("this_is_it", underscore("this is it")) self.assertEqual("thisIsIt", lcamelcase(" this is\t it\n")) self.assertEqual('abc', be(' abc\n')) self.assertEqual('', be(None)) self.assertEqual('', be(' '))
def visit_class(self, cls: ClassDefinition) -> bool: self.clswq = (WQ().add_class(camelcase(cls.name)).label( camelcase(cls.name)).description(be(cls.description))) if cls.is_a: self.clswq.parent(camelcase(cls.is_a)) if cls.abstract: self.clswq.abstract() if cls.broad_mappings: if any( str(self.namespaces.uri_for(m)) == "http://terminusdb.com/schema/system#Document" for m in cls.broad_mappings): self.clswq.parent("Document") return True
def class_or_type_name(self, name: str) -> str: """ Return the camelcase representation of clsname if it is a valid class or type. Prepend "Unknown" if the name isn't valid """ if name in self.schema.classes: return camelcase(name) elif name in self.schema.types: typ = self.schema.types[cast(TypeDefinitionName, name)] if typ.typeof: return camelcase(name) else: return typ.base else: return "Unknown_" + camelcase(name)
def gen_slot(self, slot: SlotDefinition) -> str: python_slot_name = underscore(slot.name) slot_uri, slot_curie = self.python_uri_for(slot.slot_uri) slot_model_uri, slot_model_curie = \ self.python_uri_for(self.namespaces.uri_or_curie_for(self.schema.default_prefix, python_slot_name)) domain = camelcase( slot.domain) if slot.domain and not self.schema.classes[ slot.domain].mixin else "None" # Going to omit the range on keys where the domain isn't specified (for now) if slot.domain is None and (slot.key or slot.identifier): rnge = "URIRef" else: rnge, _ = self.range_cardinality( slot, self.schema.classes[slot.domain] if slot.domain else None, True) if slot.mappings: map_texts = [ self.namespaces.curie_for(self.namespaces.uri_for(m), default_ok=True, pythonform=True) for m in slot.mappings if m != slot.slot_uri ] else: map_texts = [] if map_texts: mappings = ', mappings = [' + ', '.join(map_texts) + ']' else: mappings = '' pattern = f",\n pattern=re.compile(r'{slot.pattern}')" if slot.pattern else "" return f"""slots.{python_slot_name} = Slot(uri={slot_uri}, name="{slot.name}", curie={slot_curie},
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: qual = 'repeated ' if slot.multivalued else 'optional ' if not slot.required or slot.key else '' slotname = lcamelcase(aliased_slot_name) slot_range = camelcase(slot.range) self.relative_slot_num += 1 print(f" {qual}{slotname} {slot_range} = {self.relative_slot_num}")
def gen_type_meta(self, typ: TypeDefinition) -> str: type_class_uri = self.namespaces.uri_for(typ.uri) if type_class_uri: type_python_uri = self.namespaces.curie_for(type_class_uri, default_ok=False, pythonform=True) type_class_curie = self.namespaces.curie_for(type_class_uri, default_ok=False, pythonform=False) else: type_python_uri = None type_class_curie = None if type_class_curie: type_class_curie = f'"{type_class_curie}"' type_class_uri = type_python_uri if type_python_uri else f'URIRef("{type_class_uri}")' type_model_uri = self.namespaces.uri_or_curie_for( self.schema.default_prefix, camelcase(typ.name)) if ':/' in type_model_uri: type_model_uri = f'URIRef("{type_model_uri}")' else: ns, ln = type_model_uri.split(':', 1) ln_suffix = f".{ln}" if ln.isidentifier() else f'["{ln}"]' type_model_uri = f"{ns.upper()}{ln_suffix}" vars = [ f'type_class_uri = {type_class_uri}', f'type_class_curie = {type_class_curie}', f'type_name = "{typ.name}"', f'type_model_uri = {type_model_uri}' ] return "\n\t".join(vars)
def gen_class_meta(self, cls: ClassDefinition) -> str: if not self.gen_classvars: return '' class_class_uri = self.namespaces.uri_for(cls.class_uri) if class_class_uri: cls_python_uri = self.namespaces.curie_for(class_class_uri, default_ok=False, pythonform=True) class_class_curie = self.namespaces.curie_for(class_class_uri, default_ok=False, pythonform=False) else: cls_python_uri = None class_class_curie = None if class_class_curie: class_class_curie = f'"{class_class_curie}"' class_class_uri = cls_python_uri if cls_python_uri else f'URIRef("{class_class_uri}")' class_model_uri = self.namespaces.uri_or_curie_for( self.schema.default_prefix or "DEFAULT_", camelcase(cls.name)) if ':/' in class_model_uri: class_model_uri = f'URIRef("{class_model_uri}")' else: ns, ln = class_model_uri.split(':', 1) class_model_uri = f"{ns.upper()}.{ln}" vars = [ f'class_class_uri: ClassVar[URIRef] = {class_class_uri}', f'class_class_curie: ClassVar[str] = {class_class_curie}', f'class_name: ClassVar[str] = "{cls.name}"', f'class_model_uri: ClassVar[URIRef] = {class_model_uri}' ] return "\n\t" + "\n\t".join(vars) + "\n"
def class_identifier_path(self, cls_or_clsname: Union[str, ClassDefinition], force_non_key: bool) -> List[str]: """ Return the path closure to a class identifier if the class has a key and force_non_key is false otherwise return a dictionary closure. :param cls_or_clsname: class definition :param force_non_key: True means inlined even if the class has a key :return: path """ cls = cls_or_clsname if isinstance(cls_or_clsname, ClassDefinition) \ else self.schema.classes[ClassDefinitionName(cls_or_clsname)] # Determine whether the class has a key identifier_slot = None if not force_non_key: identifier_slot = self.class_identifier(cls) # No key or inlined, its closure is a dictionary if identifier_slot is None: return ['dict', self.class_or_type_name(cls.name)] # We're dealing with a reference pathname = camelcase(cls.name + ' ' + self.aliased_slot_name(identifier_slot)) if cls.is_a: parent_identifier_slot = self.class_identifier(cls.is_a) if parent_identifier_slot: return self.class_identifier_path(cls.is_a, False) + [pathname] return self.slot_range_path(identifier_slot) + [pathname]
def gen_enum(self, enum: EnumDefinition) -> str: enum_name = camelcase(enum.name) return f''' class {enum_name}(EnumDefinitionImpl): {self.gen_enum_comment(enum)} {self.gen_enum_description(enum, enum_name)} '''.strip()
def visit_class(self, cls: ClassDefinition) -> bool: etype = 'interface' if (cls.abstract or cls.mixin) and not cls.mixins else 'type' mixins = ', '.join([camelcase(mixin) for mixin in cls.mixins]) print(f"{etype} {camelcase(cls.name)}" + (f" implements {mixins}" if mixins else "")) print(" {") return True
def adjust_slot(self, slot: SlotDefinition) -> None: if slot.range in self.schema.classes: slot.range = ClassDefinitionName(camelcase(slot.range)) elif slot.range in self.schema.slots: slot.range = SlotDefinitionName(underscore(slot.range)) elif slot.range in self.schema.types: slot.range = TypeDefinitionName(underscore(slot.range)) slot.slot_uri = self.namespaces.uri_for(slot.slot_uri)
def visit_class(self, cls: ClassDefinition) -> bool: if self._is_hidden(cls): return False if cls.description: None ## TODO tname = camelcase(cls.name) self.columns[tname] = {} return True
def enum_identifier_path( self, enum_or_enumname: Union[str, EnumDefinition]) -> List[str]: """ Return an enum_identifier path """ return [ 'str', camelcase(enum_or_enumname.name if isinstance( enum_or_enumname, EnumDefinition) else enum_or_enumname) ]
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: slotrange = camelcase(slot.range) if slot.range in self.schema.classes or slot.range in self.schema.types or \ slot.range in self.schema.enums else "String" if slot.multivalued: slotrange = f"[{slotrange}]" if slot.required: slotrange = slotrange + '!' print(f" {lcamelcase(aliased_slot_name)}: {slotrange}")
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: if slot.range in self.schema.classes and slot.inlined: rng = f"#/definitions/{camelcase(slot.range)}" elif slot.range in self.schema.types: rng = self.schema.types[slot.range].base else: # note we assume string for non-lined complex objects rng = "string" # translate to json-schema builtins if rng == 'int': rng = 'integer' elif rng == 'Bool': rng = 'boolean' elif rng == 'str': rng = 'string' elif rng == 'float' or rng == 'double': rng = 'number' elif not rng.startswith('#'): # URIorCURIE, etc rng = 'string' if slot.inlined: # If inline we have to include redefined slots ref = JsonObj() ref['$ref'] = rng if slot.multivalued: prop = JsonObj(type="array", items=ref) else: prop = ref else: if slot.multivalued: prop = JsonObj(type="array", items={'type': rng}) else: prop = JsonObj(type=rng) if slot.description: prop.description = slot.description if slot.required: self.clsobj.required.append(underscore(aliased_slot_name)) self.clsobj.properties[underscore(aliased_slot_name)] = prop if self.topCls is not None and camelcase(self.topCls) == camelcase(cls.name) or \ self.topCls is None and cls.tree_root: self.schemaobj.properties[underscore(aliased_slot_name)] = prop
def visit_class(self, cls: ClassDefinition) -> bool: if cls.mixin or cls.abstract: return False self.clsobj = JsonObj(title=camelcase(cls.name), type='object', properties=JsonObj(), required=[], additionalProperties=False, description=be(cls.description)) return True
def gen_typedefs(self) -> str: """ Generate python type declarations for all defined types """ rval = [] for typ in self.schema.types.values(): if not typ.imported_from: typname = camelcase(typ.name) desc = f'\n\t""" {typ.description} """' if typ.description else '' if typ.typeof: parent_typename = camelcase(typ.typeof) rval.append( f'class {typname}({parent_typename}):{desc}\n\t{self.gen_type_meta(typ)}\n\n' ) else: base_base = typ.base.rsplit('.')[-1] rval.append( f'class {typname}({base_base}):{desc}\n\t{self.gen_type_meta(typ)}\n\n' ) return '\n'.join(rval)
def visit_type(self, typ: TypeDefinition) -> None: """ Visit a given type definition and write the following properties in Markdown, - Frontmatter - Description - Domain and Range constraints - Parents - Children - Used by Parameters ---------- typ: linkml_model.meta.TypeDefinition A TypeDefinition """ with open(self.dir_path(typ), 'w') as typefile: with redirect_stdout(typefile): full_path = sfx(self.namespaces._base) + (sfx( typ.imported_from) if typ.imported_from else '') type_curie = self.namespaces.uri_or_curie_for( full_path, camelcase(typ.name)) type_uri = self.namespaces.uri_for(type_curie) if type_curie.startswith( 'https://w3id.org/biolink/vocab/linkml:types/'): ref = type_curie.split('/')[-1] type_uri = f"https://linkml.github.io/linkml-model/docs/types/{ref}" type_curie = f"metatype:{ref}" elif type_uri.startswith('https://w3id.org/biolink/vocab/'): ref = type_curie.split('/')[-1] type_uri = f"https://w3id.org/biolink/vocab/types/{ref}" if typ.imported_from and 'linkml:types' in typ.imported_from: parent = 'Built-in Types' else: parent = 'Defined Types' self.frontmatter( **{ 'parent': parent, 'grand_parent': 'Types', 'title': type_curie, 'layout': 'default' }) self.element_header(typ, typ.name, type_curie, type_uri) print("| | | |") print("| --- | --- | --- |") if typ.typeof: print( f"| Parent type | | {self.class_type_link(typ.typeof)} |" ) print(f"| Root (builtin) type | | **{typ.base}** |") if typ.repr: print(f"| Representation | | {typ.repr} |")
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: #qual = 'repeated ' if slot.multivalued else 'optional ' if not slot.required or slot.key else '' slotname = underscore(aliased_slot_name) tname = camelcase(cls.name) slot_range = self.get_sql_range(slot) if slot.multivalued: linktable_name = f'{tname}_to_{slotname}' pk = self._get_primary_key(cls) if pk is None: if self.inject_primary_keys: pk = 'id' self.columns[camelcase(cls.name)][pk] = Column( pk, Text(), primary_key=True) else: raise Exception( f'Cannot have multivalued on cols with no PK: {tname} . {slotname}' ) ref = f'ref_{tname}' linkrange = None if isinstance(slot_range, ForeignKey): linkrange = Text() else: linkrange = slot_range linktable_slot = slotname if slot.singular_name is not None: linktable_slot = underscore(slot.singular_name) self.columns[linktable_name] = { linktable_slot: Column(linktable_slot, linkrange, nullable=False), ref: Column(ref, ForeignKey(f'{tname}.{pk}'), nullable=False) } is_pk = slot.identifier col = Column(slotname, slot_range, primary_key=is_pk, nullable=not slot.required) self.columns[tname][slotname] = col
def _class_or_type_uri(self, item: Union[TypeDefinition, ClassDefinition, ElementName], suffix: Optional[str] = '') -> URIorCURIE: # TODO: enums - figure this out if isinstance(item, (TypeDefinition, ClassDefinition, EnumDefinition)): cls_or_type = item else: cls_or_type = self.class_or_type_for(item) return self.namespaces.uri_for( self.namespaces.uri_or_curie_for( self.schema_defaults[cls_or_type.from_schema], camelcase(cls_or_type.name) + suffix))
def visit_schema(self, classes: Set[ClassDefinitionName] = None, directory: Optional[str] = None, load_image: bool = True, **_) -> None: if directory: os.makedirs(directory, exist_ok=True) if classes is not None: for cls in classes: if cls not in self.schema.classes: raise ValueError(f"Unknown class name: {cls}") self.box_generated = set() self.associations_generated = set() self.focus_classes = classes if classes: self.gen_classes = self.neighborhood( list(classes)).classrefs.union(classes) else: self.gen_classes = self.synopsis.roots.classrefs self.referenced = self.gen_classes self.generated = set() yumlclassdef: List[str] = [] while self.referenced.difference(self.generated): cn = sorted(list(self.referenced.difference(self.generated)), reverse=True)[0] self.generated.add(cn) assocs = self.class_associations(ClassDefinitionName(cn), cn in self.referenced) if assocs: yumlclassdef.append(assocs) else: yumlclassdef.append(self.class_box(ClassDefinitionName(cn))) yuml_url = str(YUML) + ','.join(yumlclassdef) + \ (('.' + self.format) if self.format not in ('yuml', 'svg') else '') file_suffix = '.svg' if self.format == 'yuml' else '.' + self.format if directory: self.output_file_name = os.path.join( directory, camelcase(sorted(classes)[0] if classes else self.schema.name) + file_suffix) if load_image: resp = requests.get(yuml_url, stream=True) if resp.ok: with open(self.output_file_name, 'wb') as f: for chunk in resp.iter_content(chunk_size=2048): f.write(chunk) else: self.logger.error(f"{resp.reason} accessing {yuml_url}") else: print(str(YUML) + ','.join(yumlclassdef), end='')
def visit_class(self, cls: ClassDefinition) -> bool: class_def = {} cn = camelcase(cls.name) self.add_mappings(cls) cls_prefix = self.namespaces.prefix_for(cls.class_uri) if not self.default_ns or not cls_prefix or cls_prefix != self.default_ns: class_def['@id'] = cls.class_uri if cls_prefix: self.add_prefix(cls_prefix) if class_def: self.slot_class_maps[cn] = class_def # We don't bother to visit class slots - just all slots return False
def set_from_schema(schema: SchemaDefinition) -> None: for t in [ schema.subsets, schema.classes, schema.slots, schema.types, schema.enums ]: for k in t.keys(): t[k].from_schema = schema.id if isinstance(t[k], SlotDefinition): fragment = underscore(t[k].name) else: fragment = camelcase(t[k].name) if schema.default_prefix in schema.prefixes: ns = schema.prefixes[schema.default_prefix].prefix_reference else: ns = str(URIRef(schema.id) + "/") t[k].definition_uri = f'{ns}{fragment}'
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 visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None: if slot.range in self.schema.classes: rng = camelcase(slot.range) elif slot.range in self.schema.types: # XXX Why does `linkml.utils.metamodelcore.Identifier` subclass `str`?? rng = str(self.schema.types[slot.range].uri) else: rng = "xsd:string" name = (f"{cls.name} {aliased_slot_name}" if slot.is_usage_slot else aliased_slot_name) # translate to terminusdb xsd builtins: if rng == "xsd:int": rng = "xsd:integer" elif rng == "xsd:float": rng = "xsd:double" elif rng == "xsd:language": rng = "xsd:string" if rng not in XSD_Ok and slot.range not in self.schema.classes: raise Exception( f"slot range for {name} must be schema class or supported xsd type. " f"Range {rng} is of type {type(rng)}.") self.clswq.property(underscore(name), rng, label=name, description=slot.description) if not slot.multivalued: self.clswq.max(1) if slot.required: self.clswq.min(1) if slot.is_a: self.raw_additions.append(WQ().add_quad( underscore(name), "rdfs:subPropertyOf", self.clswq.iri(underscore(slot.is_a)), "inference/main", ))
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)))