2023-08-22 04:43:02 +00:00
|
|
|
"""
|
|
|
|
Adapters to linkML classes
|
|
|
|
"""
|
2023-08-24 02:56:09 +00:00
|
|
|
import pdb
|
2023-08-31 07:01:43 +00:00
|
|
|
import re
|
|
|
|
from abc import abstractmethod
|
2023-08-24 02:56:09 +00:00
|
|
|
from typing import List, Optional
|
2023-08-25 07:22:47 +00:00
|
|
|
from nwb_schema_language import Dataset, Group, ReferenceDtype, CompoundDtype, DTypeType
|
2023-08-24 02:56:09 +00:00
|
|
|
from nwb_linkml.adapters.adapter import Adapter, BuildResult
|
2023-08-22 04:43:02 +00:00
|
|
|
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
2023-08-25 07:22:47 +00:00
|
|
|
from nwb_linkml.maps import QUANTITY_MAP
|
|
|
|
from nwb_linkml.lang_elements import Arraylike
|
2023-08-22 04:43:02 +00:00
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
CAMEL_TO_SNAKE = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
|
|
|
|
"""
|
|
|
|
Convert camel case to snake case
|
|
|
|
|
|
|
|
courtesy of: https://stackoverflow.com/a/12867228
|
|
|
|
"""
|
|
|
|
|
|
|
|
def camel_to_snake(name:str) -> str:
|
|
|
|
"""
|
|
|
|
Convert camel case to snake case
|
|
|
|
|
|
|
|
courtesy of: https://stackoverflow.com/a/12867228
|
|
|
|
"""
|
|
|
|
return CAMEL_TO_SNAKE.sub(r'_\1', name).lower()
|
|
|
|
|
2023-08-22 04:43:02 +00:00
|
|
|
class ClassAdapter(Adapter):
|
|
|
|
"""
|
2023-08-31 07:01:43 +00:00
|
|
|
Abstract adapter to class-like things in linkml, holds methods common to
|
|
|
|
both DatasetAdapter and GroupAdapter
|
2023-08-22 04:43:02 +00:00
|
|
|
"""
|
|
|
|
cls: Dataset | Group
|
2023-08-24 02:56:09 +00:00
|
|
|
parent: Optional['ClassAdapter'] = None
|
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
@abstractmethod
|
|
|
|
def build(self) -> BuildResult:
|
|
|
|
"""
|
|
|
|
Make this abstract so it can't be instantiated directly.
|
|
|
|
|
|
|
|
Subclasses call :meth:`.build_base` to get the basics true of both groups and datasets
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def build_base(self, extra_attrs: Optional[List[SlotDefinition]]=None) -> BuildResult:
|
|
|
|
"""
|
|
|
|
Build the basic class and attributes before adding any specific
|
|
|
|
modifications for groups or datasets.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Build this class
|
|
|
|
#name = self._get_full_name()
|
2023-09-01 03:56:21 +00:00
|
|
|
kwargs = {}
|
2023-08-31 07:01:43 +00:00
|
|
|
if self.parent is not None:
|
2023-09-01 03:56:21 +00:00
|
|
|
kwargs['name'] = self._get_full_name()
|
2023-08-31 07:01:43 +00:00
|
|
|
else:
|
2023-09-01 03:56:21 +00:00
|
|
|
kwargs['name'] = self._get_attr_name()
|
|
|
|
kwargs['tree_root'] = True
|
2023-08-31 07:01:43 +00:00
|
|
|
|
2023-09-01 03:56:21 +00:00
|
|
|
# Attributes
|
2023-08-31 07:01:43 +00:00
|
|
|
name_slot = self.build_name_slot()
|
2023-09-01 03:56:21 +00:00
|
|
|
kwargs['attributes'] = [name_slot]
|
|
|
|
# Get vanilla top-level attributes
|
|
|
|
kwargs['attributes'].extend(self.build_attrs(self.cls))
|
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
if extra_attrs is not None:
|
|
|
|
if isinstance(extra_attrs, SlotDefinition):
|
|
|
|
extra_attrs = [extra_attrs]
|
2023-09-01 03:56:21 +00:00
|
|
|
kwargs['attributes'].extend(extra_attrs)
|
|
|
|
kwargs['description'] = self.cls.doc
|
|
|
|
kwargs['is_a'] = self.cls.neurodata_type_inc
|
2023-08-31 07:01:43 +00:00
|
|
|
|
|
|
|
cls = ClassDefinition(
|
2023-09-01 03:56:21 +00:00
|
|
|
**kwargs
|
2023-08-31 07:01:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
slots = []
|
|
|
|
if self.parent is not None:
|
|
|
|
slots.append(self.build_self_slot())
|
|
|
|
|
|
|
|
res = BuildResult(
|
|
|
|
classes = [cls],
|
|
|
|
slots = slots
|
|
|
|
)
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-08-24 02:56:09 +00:00
|
|
|
def _get_full_name(self) -> str:
|
|
|
|
"""The full name of the object in the generated linkml
|
2023-08-22 04:43:02 +00:00
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
Distinct from 'name' which is the thing that's used to define position in
|
|
|
|
a hierarchical data setting
|
|
|
|
"""
|
2023-08-22 04:43:02 +00:00
|
|
|
if self.cls.neurodata_type_def:
|
|
|
|
name = self.cls.neurodata_type_def
|
2023-08-24 02:56:09 +00:00
|
|
|
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)
|
2023-08-29 05:16:58 +00:00
|
|
|
name = '__'.join(name_parts)
|
2023-08-24 02:56:09 +00:00
|
|
|
elif self.cls.neurodata_type_inc is not None:
|
|
|
|
# again, this is against the schema, but is common
|
|
|
|
name = self.cls.neurodata_type_inc
|
2023-08-22 04:43:02 +00:00
|
|
|
else:
|
2023-08-24 02:56:09 +00:00
|
|
|
raise ValueError('Not sure what our name is!')
|
|
|
|
|
|
|
|
|
|
|
|
return name
|
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
def _get_attr_name(self) -> str:
|
2023-08-24 02:56:09 +00:00
|
|
|
"""
|
2023-08-31 07:01:43 +00:00
|
|
|
Get the name to use as the attribute name,
|
|
|
|
again distinct from the actual name of the instantiated object
|
2023-08-24 02:56:09 +00:00
|
|
|
"""
|
|
|
|
# return self._get_full_name()
|
|
|
|
name = None
|
|
|
|
if self.cls.neurodata_type_def:
|
2023-08-31 07:01:43 +00:00
|
|
|
#name = camel_to_snake(self.cls.neurodata_type_def)
|
2023-08-24 02:56:09 +00:00
|
|
|
name = self.cls.neurodata_type_def
|
|
|
|
elif self.cls.name is not None:
|
|
|
|
# we do have a unique name
|
2023-08-22 04:43:02 +00:00
|
|
|
name = self.cls.name
|
2023-08-24 02:56:09 +00:00
|
|
|
elif self.cls.neurodata_type_inc:
|
2023-08-31 07:01:43 +00:00
|
|
|
#name = camel_to_snake(self.cls.neurodata_type_inc)
|
2023-08-24 02:56:09 +00:00
|
|
|
name = self.cls.neurodata_type_inc
|
|
|
|
|
|
|
|
if name is None:
|
|
|
|
raise ValueError(f'Class has no name!: {self.cls}')
|
|
|
|
|
|
|
|
return name
|
2023-08-22 04:43:02 +00:00
|
|
|
|
2023-08-25 07:22:47 +00:00
|
|
|
def handle_dtype(self, dtype: DTypeType | None) -> str:
|
2023-08-24 02:56:09 +00:00
|
|
|
if isinstance(dtype, ReferenceDtype):
|
|
|
|
return dtype.target_type
|
2023-08-25 07:22:47 +00:00
|
|
|
elif dtype is None or dtype == []:
|
|
|
|
# Some ill-defined datasets are "abstract" despite that not being in the schema language
|
|
|
|
return 'AnyType'
|
|
|
|
elif isinstance(dtype, list) and isinstance(dtype[0], CompoundDtype):
|
|
|
|
# there is precisely one class that uses compound dtypes:
|
|
|
|
# TimeSeriesReferenceVectorData
|
|
|
|
# compoundDtypes are able to define a ragged table according to the schema
|
|
|
|
# but are used in this single case equivalently to attributes.
|
|
|
|
# so we'll... uh... treat them as slots.
|
|
|
|
# TODO
|
|
|
|
return 'AnyType'
|
|
|
|
#raise NotImplementedError('got distracted, need to implement')
|
|
|
|
|
2023-08-24 02:56:09 +00:00
|
|
|
else:
|
2023-08-25 07:22:47 +00:00
|
|
|
# flat dtype
|
2023-08-24 02:56:09 +00:00
|
|
|
return dtype
|
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
def build_name_slot(self) -> SlotDefinition:
|
|
|
|
"""
|
|
|
|
If a class has a name, then that name should be a slot with a
|
|
|
|
fixed value.
|
2023-08-22 04:43:02 +00:00
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
If a class does not have a name, then name should be a required attribute
|
|
|
|
|
|
|
|
References:
|
|
|
|
https://github.com/NeurodataWithoutBorders/nwb-schema/issues/552#issuecomment-1700319001
|
|
|
|
|
|
|
|
Returns:
|
2023-08-24 02:56:09 +00:00
|
|
|
|
|
|
|
"""
|
2023-08-31 07:01:43 +00:00
|
|
|
if self.cls.name:
|
|
|
|
name_slot = SlotDefinition(
|
|
|
|
name='name',
|
|
|
|
required=True,
|
2023-08-31 22:36:58 +00:00
|
|
|
ifabsent=f'string({self.cls.name})',
|
2023-08-31 07:01:43 +00:00
|
|
|
equals_string=self.cls.name,
|
|
|
|
range='string'
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
name_slot = SlotDefinition(
|
|
|
|
name='name',
|
|
|
|
required=True,
|
|
|
|
range='string'
|
|
|
|
)
|
|
|
|
return name_slot
|
2023-08-24 02:56:09 +00:00
|
|
|
|
2023-08-31 07:01:43 +00:00
|
|
|
def build_self_slot(self) -> SlotDefinition:
|
2023-08-24 02:56:09 +00:00
|
|
|
"""
|
2023-08-31 07:01:43 +00:00
|
|
|
If we are a child class, we make a slot so our parent can refer to us
|
|
|
|
"""
|
|
|
|
return SlotDefinition(
|
|
|
|
name=self._get_attr_name(),
|
|
|
|
description=self.cls.doc,
|
|
|
|
range=self._get_full_name(),
|
|
|
|
**QUANTITY_MAP[self.cls.quantity]
|
|
|
|
)
|
2023-08-24 02:56:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-25 07:22:47 +00:00
|
|
|
|
2023-08-24 02:56:09 +00:00
|
|
|
|