Skip to content

hololinked.td.data_schema.DataSchema

Bases: Schema

Implements Data Schema, usually used to represent payloads of properties, actions and events in a WoT Thing Description.

Source code in hololinked/hololinked/td/data_schema.py
class DataSchema(Schema):
    """
    Implements Data Schema, usually used to represent payloads of properties, actions and events in a
    WoT Thing Description.

    - [Vocabulary Definitions](https://www.w3.org/TR/wot-thing-description11/#sec-data-schema-vocabulary-definition)
    - [Supported Fields](https://www.w3.org/TR/wot-thing-description11/#data-schema-fields)
    """

    title: str = None
    titles: Optional[dict[str, str]] = None
    description: str = None
    descriptions: Optional[dict[str, str]] = None
    const: Optional[bool] = None
    default: Optional[Any] = None
    readOnly: Optional[bool] = None
    writeOnly: Optional[bool] = None
    # write only can be considered as actions with no return value, so not used in this repository
    format: Optional[str] = None
    unit: Optional[str] = None
    type: Optional[str] = None
    oneOf: Optional[list[JSON]] = None

    model_config = ConfigDict(extra="allow")
    _custom_schema_generators: ClassVar = dict()

    def __init__(self):
        super().__init__()

    def ds_build_fields_from_property(self, property: Property) -> None:
        """populates schema information from property descriptor object"""
        self.title = get_summary(property.doc)
        if property.constant:
            self.const = property.constant
        if property.readonly:
            self.readOnly = property.readonly
        if property.default is not None:
            self.default = property.default
        if property.doc:
            self.description = Schema.format_doc(property.doc)
            if self.title and self.description.startswith(self.title):
                self.description.lstrip(self.title)
                self.description.lstrip(".").lstrip()
                self.title = ""
        if property.metadata and property.metadata.get("unit", None) is not None:
            self.unit = property.metadata["unit"]
        if property.allow_None:
            if (hasattr(self, "type") and self.type is not None) or (hasattr(self, "oneOf") and self.oneOf):
                self._move_own_type_to_oneOf()
            if not hasattr(self, "oneOf") or self.oneOf is None:
                self.oneOf = []
            if not any(types.get("type", NotImplemented) in [None, "null"] for types in self.oneOf):
                self.oneOf.append(dict(type="null"))
        if not self.title:
            del self.title

    # & _ds prefix is used to avoid name conflicts with PropertyAffordance class
    # you dont know what you are building, whether the data schema or something else when viewed from property affordance
    def ds_build_from_property(self, property: Property) -> None:
        """
        generates the schema specific to the property type,
        calls `ds_build_fields_from_property()` after choosing the right type
        """
        if self._custom_schema_generators.get(property, NotImplemented) is not NotImplemented:
            data_schema = self._custom_schema_generators[property]()
        elif isinstance(property, Property) and property.model is not None:
            base_data_schema = DataSchema()
            if isinstance(property.model, dict):
                given_data_schema = property.model
            elif issubklass(property.model, (BaseModel, RootModel)):
                from .pydantic_extensions import type_to_dataschema

                given_data_schema = type_to_dataschema(property.model)
            if property.allow_None:
                base_data_schema.oneOf = []
                base_data_schema.oneOf.append(dict(type="null"))
            if base_data_schema.oneOf:  # allow_None = True
                base_data_schema.oneOf.append(given_data_schema)
            else:
                for key, value in given_data_schema.items():
                    setattr(base_data_schema, key, value)
            data_schema = base_data_schema
        elif isinstance(property, (String, Filename, Foldername, Path)):
            data_schema = StringSchema()
        elif isinstance(property, (Number, Integer)):
            data_schema = NumberSchema()
        elif isinstance(property, Boolean):
            data_schema = BooleanSchema()
        elif isinstance(property, (List, TypedList, Tuple, TupleSelector)):
            data_schema = ArraySchema()
        elif isinstance(property, Selector):
            data_schema = EnumSchema()
        elif isinstance(property, (TypedDict, TypedKeyMappingsDict)):
            data_schema = ObjectSchema()
        elif isinstance(property, ClassSelector):
            data_schema = SelectorSchema()
        else:
            raise TypeError(
                f"WoT schema generator for this descriptor/property is not implemented. name {property.name} & type {type(property)}"
            )

        data_schema.ds_build_fields_from_property(property)
        for field_name in data_schema.model_dump(exclude_unset=True).keys():
            field_value = getattr(data_schema, field_name, NotImplemented)
            if field_value is not NotImplemented:
                setattr(self, field_name, field_value)

    def _move_own_type_to_oneOf(self):
        """move a type to oneOf"""
        pass

Functions

ds_build_fields_from_property

ds_build_fields_from_property(property: Property) -> None

populates schema information from property descriptor object

Source code in hololinked/hololinked/td/data_schema.py
def ds_build_fields_from_property(self, property: Property) -> None:
    """populates schema information from property descriptor object"""
    self.title = get_summary(property.doc)
    if property.constant:
        self.const = property.constant
    if property.readonly:
        self.readOnly = property.readonly
    if property.default is not None:
        self.default = property.default
    if property.doc:
        self.description = Schema.format_doc(property.doc)
        if self.title and self.description.startswith(self.title):
            self.description.lstrip(self.title)
            self.description.lstrip(".").lstrip()
            self.title = ""
    if property.metadata and property.metadata.get("unit", None) is not None:
        self.unit = property.metadata["unit"]
    if property.allow_None:
        if (hasattr(self, "type") and self.type is not None) or (hasattr(self, "oneOf") and self.oneOf):
            self._move_own_type_to_oneOf()
        if not hasattr(self, "oneOf") or self.oneOf is None:
            self.oneOf = []
        if not any(types.get("type", NotImplemented) in [None, "null"] for types in self.oneOf):
            self.oneOf.append(dict(type="null"))
    if not self.title:
        del self.title

ds_build_from_property

ds_build_from_property(property: Property) -> None

generates the schema specific to the property type, calls ds_build_fields_from_property() after choosing the right type

Source code in hololinked/hololinked/td/data_schema.py
def ds_build_from_property(self, property: Property) -> None:
    """
    generates the schema specific to the property type,
    calls `ds_build_fields_from_property()` after choosing the right type
    """
    if self._custom_schema_generators.get(property, NotImplemented) is not NotImplemented:
        data_schema = self._custom_schema_generators[property]()
    elif isinstance(property, Property) and property.model is not None:
        base_data_schema = DataSchema()
        if isinstance(property.model, dict):
            given_data_schema = property.model
        elif issubklass(property.model, (BaseModel, RootModel)):
            from .pydantic_extensions import type_to_dataschema

            given_data_schema = type_to_dataschema(property.model)
        if property.allow_None:
            base_data_schema.oneOf = []
            base_data_schema.oneOf.append(dict(type="null"))
        if base_data_schema.oneOf:  # allow_None = True
            base_data_schema.oneOf.append(given_data_schema)
        else:
            for key, value in given_data_schema.items():
                setattr(base_data_schema, key, value)
        data_schema = base_data_schema
    elif isinstance(property, (String, Filename, Foldername, Path)):
        data_schema = StringSchema()
    elif isinstance(property, (Number, Integer)):
        data_schema = NumberSchema()
    elif isinstance(property, Boolean):
        data_schema = BooleanSchema()
    elif isinstance(property, (List, TypedList, Tuple, TupleSelector)):
        data_schema = ArraySchema()
    elif isinstance(property, Selector):
        data_schema = EnumSchema()
    elif isinstance(property, (TypedDict, TypedKeyMappingsDict)):
        data_schema = ObjectSchema()
    elif isinstance(property, ClassSelector):
        data_schema = SelectorSchema()
    else:
        raise TypeError(
            f"WoT schema generator for this descriptor/property is not implemented. name {property.name} & type {type(property)}"
        )

    data_schema.ds_build_fields_from_property(property)
    for field_name in data_schema.model_dump(exclude_unset=True).keys():
        field_value = getattr(data_schema, field_name, NotImplemented)
        if field_value is not NotImplemented:
            setattr(self, field_name, field_value)

TD Supported Fields

field supported meaning default usage
title ✔️ Provides a human-readable title label of Property or first line of docstring
titles Provides multi-language human-readable titles to be manually set
description ✔️ Provides additional human-readable information cleaned docstring of Property (doc value)
descriptions Provides multi-language human-readable descriptions to be manually set
const ✔️ true when value will remain constant Property.constant value
default ✔️ Provides a default value Property.default value
format format pattern such as "date-time", "email", "uri", etc. to be manually set, will be supported in a future release
readOnly ✔️ true when value is read-only Property.readonly value
writeOnly true when value is write-only It is assumed that a property always has an associated value that can be read
unit ✔️ Provides a human-readable unit Property.metadata["unit"] value
type ✔️ Provides a type for the property typed inferred from specific subclass of Property, pydantic models are considered as an object currently even when having only one field or a root model (will be fixed in a future release
oneOf ✔️ Provides a list of possible values Usually for properties with allow_None apart from its own type

See subclasses for more specific fields under same topic.