mirror of
https://github.com/p2p-ld/nwb-linkml.git
synced 2024-11-10 00:34:29 +00:00
140 lines
No EOL
4.5 KiB
Python
140 lines
No EOL
4.5 KiB
Python
"""
|
|
Adapters to linkML classes
|
|
"""
|
|
import pdb
|
|
from typing import List, Optional
|
|
from nwb_schema_language import Dataset, Group, ReferenceDtype, DTypeType
|
|
from nwb_linkml.adapters.adapter import Adapter, BuildResult
|
|
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
|
|
|
|
|
class ClassAdapter(Adapter):
|
|
"""
|
|
Adapter to class-like things in linkml, including datasets and groups
|
|
"""
|
|
cls: Dataset | Group
|
|
parent: Optional['ClassAdapter'] = None
|
|
|
|
def _get_full_name(self) -> str:
|
|
"""The full name of the object in the generated linkml
|
|
|
|
Distinct from 'name' which is the thing that's often used in """
|
|
if self.cls.neurodata_type_def:
|
|
name = self.cls.neurodata_type_def
|
|
elif self.cls.name is not None:
|
|
# not necessarily a unique name, so we combine parent names
|
|
name_parts = []
|
|
if self.parent is not None:
|
|
name_parts.append(self.parent._get_full_name())
|
|
|
|
name_parts.append(self.cls.name)
|
|
name = '_'.join(name_parts)
|
|
elif self.cls.neurodata_type_inc is not None:
|
|
# again, this is against the schema, but is common
|
|
name = self.cls.neurodata_type_inc
|
|
else:
|
|
raise ValueError('Not sure what our name is!')
|
|
|
|
|
|
return name
|
|
|
|
def _get_name(self) -> str:
|
|
"""
|
|
Get the "regular" name, which is used as the name of the attr
|
|
|
|
Returns:
|
|
|
|
"""
|
|
# return self._get_full_name()
|
|
name = None
|
|
if self.cls.neurodata_type_def:
|
|
name = self.cls.neurodata_type_def
|
|
elif self.cls.name is not None:
|
|
# we do have a unique name
|
|
name = self.cls.name
|
|
elif self.cls.neurodata_type_inc:
|
|
# group members can be anonymous? this violates the schema but is common
|
|
name = self.cls.neurodata_type_inc
|
|
|
|
if name is None:
|
|
raise ValueError(f'Class has no name!: {self.cls}')
|
|
|
|
return name
|
|
|
|
def handle_dtype(self, dtype: DTypeType):
|
|
if isinstance(dtype, ReferenceDtype):
|
|
return dtype.target_type
|
|
else:
|
|
return dtype
|
|
|
|
def build_attrs(self, cls: Dataset | Group) -> List[SlotDefinition]:
|
|
attrs = [
|
|
SlotDefinition(
|
|
name=attr.name,
|
|
description=attr.doc,
|
|
range=self.handle_dtype(attr.dtype)
|
|
) for attr in cls.attributes
|
|
]
|
|
|
|
return attrs
|
|
|
|
def build_subclasses(self, cls: Dataset | Group) -> BuildResult:
|
|
"""
|
|
Build nested groups and datasets
|
|
|
|
Create ClassDefinitions for each, but then also create SlotDefinitions that
|
|
will be used as attributes linking the main class to the subclasses
|
|
"""
|
|
# build and flatten nested classes
|
|
nested_classes = [ClassAdapter(cls=dset, parent=self) for dset in cls.datasets]
|
|
nested_classes.extend([ClassAdapter(cls=grp, parent=self) for grp in cls.groups])
|
|
nested_res = BuildResult()
|
|
for subclass in nested_classes:
|
|
this_slot = SlotDefinition(
|
|
name=subclass._get_name(),
|
|
description=subclass.cls.doc,
|
|
range=subclass._get_full_name()
|
|
)
|
|
nested_res.slots.append(this_slot)
|
|
|
|
if subclass.cls.name is None and subclass.cls.neurodata_type_def is None:
|
|
# anonymous group that's just an inc, we only need the slot since the class is defined elsewhere
|
|
continue
|
|
|
|
this_build = subclass.build()
|
|
nested_res += this_build
|
|
return nested_res
|
|
|
|
|
|
def build(self) -> BuildResult:
|
|
|
|
# Build this class
|
|
if self.parent is not None:
|
|
name = self._get_full_name()
|
|
else:
|
|
name = self._get_name()
|
|
# if name == 'TimeSeries':
|
|
# pdb.set_trace()
|
|
|
|
# Get vanilla top-level attributes
|
|
attrs = self.build_attrs(self.cls)
|
|
|
|
# unnest and build subclasses in datasets and groups
|
|
if isinstance(self.cls, Group):
|
|
# only groups have sub-datasets and sub-groups
|
|
nested_res = self.build_subclasses(self.cls)
|
|
attrs.extend(nested_res.slots)
|
|
else:
|
|
nested_res = BuildResult()
|
|
|
|
cls = ClassDefinition(
|
|
name = name,
|
|
is_a = self.cls.neurodata_type_inc,
|
|
description=self.cls.doc,
|
|
attributes=attrs
|
|
)
|
|
res = BuildResult(
|
|
classes = [cls, *nested_res.classes]
|
|
)
|
|
|
|
return res |