Skip to content

widget

Interface of Metador widget plugins.

Widget

Bases: ABC

Base class for metador widgets.

Source code in src/metador_core/widget/__init__.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
class Widget(ABC):
    """Base class for metador widgets."""

    _args: Dict[str, Any]
    """Additional passed arguments (e.g. from the dashboard)."""

    _node: MetadorNode
    """Container node passed to the widget."""

    _meta: MetadataSchema
    """Metadata object to be used as the starting point (e.g. widget config)."""

    _server: WidgetServer
    """Widget-backing server object to use (e.g. to access container files from frontend)."""

    Plugin: ClassVar[WidgetPlugin]

    def __init__(
        self,
        node: MetadorNode,
        schema_name: str = "",
        schema_version: Optional[SemVerTuple] = None,
        *,
        server: Optional[WidgetServer] = None,
        container_id: Optional[str] = None,
        metadata: Optional[MetadataSchema] = None,
        max_width: Optional[int] = None,
        max_height: Optional[int] = None,
        keep_ratio: bool = False,
    ):
        """Instantiate a widget for a node.

        If no schema name is provided, the widget will try to pick the first metadata object
        from the node that is an instance of a supported schema, in the listed order.

        If no server is provided, a stand-alone server is started (e.g. for use in a notebook).

        If a metadata object is passed explicitly, it will be used instead of trying to
        retrieve one from the node.
        """
        # NOTE: we restrict the node so that widgets don't try to escape their scope
        self._node = node.restrict(read_only=True, local_only=True)
        # NOTE: if no container_id is passed, we assume its jupyter mode (based on the metador UUIDs)
        self._container_id = container_id or str(node.metador.container_uuid)

        # if no server passed, we're in Jupyter mode - use standalone
        srv: WidgetServer
        if server is not None:
            srv = server
        else:
            from .jupyter.standalone import running, widget_server

            if not running():
                raise ValueError(
                    "No widget server passed and standalone server not running!"
                )
            srv = widget_server()
        self._server = srv

        # setup correct metadata
        if metadata is not None:
            if not self.supports_meta(metadata):
                msg = "Passed metadata is not instance of a supported schema!"
                raise ValueError(msg)
            self._meta = metadata
        else:
            if not schema_name:
                for schemaref in self.Plugin.supports:
                    if node.meta.get(schemaref.name):
                        schema_name = schemaref.name
                        break
            if not schema_name:
                raise ValueError("The node does not contain any suitable metadata!")

            if metadata := node.meta.get(schema_name, schema_version):
                self._meta = metadata
            else:
                raise ValueError("The node does not contain '{schema_name}' metadata!")

        # maximum width and height that can be used (if None, unlimited)
        self._w: Optional[int] = max_width
        self._h: Optional[int] = max_height

        # recalibrate maximum width and height of widgets to preserve ration, if possible + desired
        can_scale = self._meta.width is not None and self._meta.height is not None
        if keep_ratio and can_scale:
            scale_factor = min(
                self._h / self._meta.height.value, self._w / self._meta.width.value
            )
            self._w = int(self._meta.width.value * scale_factor)
            self._h = int(self._meta.height.value * scale_factor)

        # widget-specific setup hook
        self.setup()

    def file_data(self, node: Optional[MetadorDataset] = None) -> bytes:
        """Return data at passed dataset node as bytes.

        If no node passed, will use the widget root node (if it is a dataset).
        """
        node = node or self._node
        if not isinstance(node, MetadorDataset):
            raise ValueError(
                f"Passed node {node.name} does not look like a dataset node!"
            )
        return node[()].tolist()

    def file_url(self, node: Optional[MetadorNode] = None) -> str:
        """Return URL resolving to the data at given node.

        If no node passed, will use the widget root node (if it is a dataset).
        """
        node = node or self._node
        if not isinstance(node, MetadorDataset):
            raise ValueError(
                f"Passed node {node.name} does not look like a dataset node!"
            )
        return self._server.file_url_for(self._container_id, node)

    @classmethod
    def supports(cls, *schemas: PluginRef) -> bool:
        """Return whether any (exact) schema is supported (version-compatible) by widget."""
        return any(
            any(map(lambda sref: sref.supports(schema), cls.Plugin.supports))
            for schema in schemas
        )

    @classmethod
    def supports_meta(cls, obj: MetadataSchema) -> bool:
        """Return whether widget supports the specific metadata object.

        The passed object is assumed to be of one of the supported schema types.

        Default implementation will just check that the object is of a supported schema.

        Override to constrain further (e.g. check field values).

        This method affects the dashboard widget selection process and is used
        to check a metadata object if directly passed to `__init__`.
        """
        return cls.supports(type(obj).Plugin.ref())

    def setup(self):  # noqa: B027  # implementing it is not mandatory
        """Check that passed node is valid and do preparations.

        If multiple supported schemas are listed, case splitting based on the
        schema type should be done here to minimize logic in the rendering.

        Everything that instances can reuse, especially if it is computationally
        expensive, should also be done here.

        In case the widget is not able to work with the given node and metadata,
        it will raise a `ValueError`.
        """

    @abstractmethod
    def show(self) -> Viewable:
        """Return a fresh Panel widget representing the node data and/or metadata.

        If width and height were provided during initialization, the widget is supposed
        to fit within these dimensions, not exceed them and if possible, usefully
        fill up the space.

        This method assumes that the widget is fully initialized and setup is completed.
        """
        raise NotImplementedError

__init__

__init__(
    node: MetadorNode,
    schema_name: str = "",
    schema_version: Optional[SemVerTuple] = None,
    *,
    server: Optional[WidgetServer] = None,
    container_id: Optional[str] = None,
    metadata: Optional[MetadataSchema] = None,
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
    keep_ratio: bool = False
)

Instantiate a widget for a node.

If no schema name is provided, the widget will try to pick the first metadata object from the node that is an instance of a supported schema, in the listed order.

If no server is provided, a stand-alone server is started (e.g. for use in a notebook).

If a metadata object is passed explicitly, it will be used instead of trying to retrieve one from the node.

Source code in src/metador_core/widget/__init__.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def __init__(
    self,
    node: MetadorNode,
    schema_name: str = "",
    schema_version: Optional[SemVerTuple] = None,
    *,
    server: Optional[WidgetServer] = None,
    container_id: Optional[str] = None,
    metadata: Optional[MetadataSchema] = None,
    max_width: Optional[int] = None,
    max_height: Optional[int] = None,
    keep_ratio: bool = False,
):
    """Instantiate a widget for a node.

    If no schema name is provided, the widget will try to pick the first metadata object
    from the node that is an instance of a supported schema, in the listed order.

    If no server is provided, a stand-alone server is started (e.g. for use in a notebook).

    If a metadata object is passed explicitly, it will be used instead of trying to
    retrieve one from the node.
    """
    # NOTE: we restrict the node so that widgets don't try to escape their scope
    self._node = node.restrict(read_only=True, local_only=True)
    # NOTE: if no container_id is passed, we assume its jupyter mode (based on the metador UUIDs)
    self._container_id = container_id or str(node.metador.container_uuid)

    # if no server passed, we're in Jupyter mode - use standalone
    srv: WidgetServer
    if server is not None:
        srv = server
    else:
        from .jupyter.standalone import running, widget_server

        if not running():
            raise ValueError(
                "No widget server passed and standalone server not running!"
            )
        srv = widget_server()
    self._server = srv

    # setup correct metadata
    if metadata is not None:
        if not self.supports_meta(metadata):
            msg = "Passed metadata is not instance of a supported schema!"
            raise ValueError(msg)
        self._meta = metadata
    else:
        if not schema_name:
            for schemaref in self.Plugin.supports:
                if node.meta.get(schemaref.name):
                    schema_name = schemaref.name
                    break
        if not schema_name:
            raise ValueError("The node does not contain any suitable metadata!")

        if metadata := node.meta.get(schema_name, schema_version):
            self._meta = metadata
        else:
            raise ValueError("The node does not contain '{schema_name}' metadata!")

    # maximum width and height that can be used (if None, unlimited)
    self._w: Optional[int] = max_width
    self._h: Optional[int] = max_height

    # recalibrate maximum width and height of widgets to preserve ration, if possible + desired
    can_scale = self._meta.width is not None and self._meta.height is not None
    if keep_ratio and can_scale:
        scale_factor = min(
            self._h / self._meta.height.value, self._w / self._meta.width.value
        )
        self._w = int(self._meta.width.value * scale_factor)
        self._h = int(self._meta.height.value * scale_factor)

    # widget-specific setup hook
    self.setup()

file_data

file_data(node: Optional[MetadorDataset] = None) -> bytes

Return data at passed dataset node as bytes.

If no node passed, will use the widget root node (if it is a dataset).

Source code in src/metador_core/widget/__init__.py
126
127
128
129
130
131
132
133
134
135
136
def file_data(self, node: Optional[MetadorDataset] = None) -> bytes:
    """Return data at passed dataset node as bytes.

    If no node passed, will use the widget root node (if it is a dataset).
    """
    node = node or self._node
    if not isinstance(node, MetadorDataset):
        raise ValueError(
            f"Passed node {node.name} does not look like a dataset node!"
        )
    return node[()].tolist()

file_url

file_url(node: Optional[MetadorNode] = None) -> str

Return URL resolving to the data at given node.

If no node passed, will use the widget root node (if it is a dataset).

Source code in src/metador_core/widget/__init__.py
138
139
140
141
142
143
144
145
146
147
148
def file_url(self, node: Optional[MetadorNode] = None) -> str:
    """Return URL resolving to the data at given node.

    If no node passed, will use the widget root node (if it is a dataset).
    """
    node = node or self._node
    if not isinstance(node, MetadorDataset):
        raise ValueError(
            f"Passed node {node.name} does not look like a dataset node!"
        )
    return self._server.file_url_for(self._container_id, node)

supports classmethod

supports(*schemas: PluginRef) -> bool

Return whether any (exact) schema is supported (version-compatible) by widget.

Source code in src/metador_core/widget/__init__.py
150
151
152
153
154
155
156
@classmethod
def supports(cls, *schemas: PluginRef) -> bool:
    """Return whether any (exact) schema is supported (version-compatible) by widget."""
    return any(
        any(map(lambda sref: sref.supports(schema), cls.Plugin.supports))
        for schema in schemas
    )

supports_meta classmethod

supports_meta(obj: MetadataSchema) -> bool

Return whether widget supports the specific metadata object.

The passed object is assumed to be of one of the supported schema types.

Default implementation will just check that the object is of a supported schema.

Override to constrain further (e.g. check field values).

This method affects the dashboard widget selection process and is used to check a metadata object if directly passed to __init__.

Source code in src/metador_core/widget/__init__.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@classmethod
def supports_meta(cls, obj: MetadataSchema) -> bool:
    """Return whether widget supports the specific metadata object.

    The passed object is assumed to be of one of the supported schema types.

    Default implementation will just check that the object is of a supported schema.

    Override to constrain further (e.g. check field values).

    This method affects the dashboard widget selection process and is used
    to check a metadata object if directly passed to `__init__`.
    """
    return cls.supports(type(obj).Plugin.ref())

setup

setup()

Check that passed node is valid and do preparations.

If multiple supported schemas are listed, case splitting based on the schema type should be done here to minimize logic in the rendering.

Everything that instances can reuse, especially if it is computationally expensive, should also be done here.

In case the widget is not able to work with the given node and metadata, it will raise a ValueError.

Source code in src/metador_core/widget/__init__.py
173
174
175
176
177
178
179
180
181
182
183
184
def setup(self):  # noqa: B027  # implementing it is not mandatory
    """Check that passed node is valid and do preparations.

    If multiple supported schemas are listed, case splitting based on the
    schema type should be done here to minimize logic in the rendering.

    Everything that instances can reuse, especially if it is computationally
    expensive, should also be done here.

    In case the widget is not able to work with the given node and metadata,
    it will raise a `ValueError`.
    """

show abstractmethod

show() -> Viewable

Return a fresh Panel widget representing the node data and/or metadata.

If width and height were provided during initialization, the widget is supposed to fit within these dimensions, not exceed them and if possible, usefully fill up the space.

This method assumes that the widget is fully initialized and setup is completed.

Source code in src/metador_core/widget/__init__.py
186
187
188
189
190
191
192
193
194
195
196
@abstractmethod
def show(self) -> Viewable:
    """Return a fresh Panel widget representing the node data and/or metadata.

    If width and height were provided during initialization, the widget is supposed
    to fit within these dimensions, not exceed them and if possible, usefully
    fill up the space.

    This method assumes that the widget is fully initialized and setup is completed.
    """
    raise NotImplementedError

WidgetPlugin

Bases: PluginBase

Source code in src/metador_core/widget/__init__.py
207
208
209
210
211
212
213
214
215
class WidgetPlugin(pg.PluginBase):
    supports: Annotated[List[SchemaPluginRef], Field(min_items=1)]  # type: ignore
    """Return list of schemas supported by this widget."""

    primary: bool = True
    """Return whether the widget is a primary choice.

    If False, will not be used automatically by dashboard.
    """

supports instance-attribute

supports: Annotated[
    List[SchemaPluginRef], Field(min_items=1)
]

Return list of schemas supported by this widget.

primary class-attribute instance-attribute

primary: bool = True

Return whether the widget is a primary choice.

If False, will not be used automatically by dashboard.

PGWidget

Bases: PluginGroup[Widget]

Widget plugin group interface.

Source code in src/metador_core/widget/__init__.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
class PGWidget(pg.PluginGroup[Widget]):
    """Widget plugin group interface."""

    class Plugin:
        name = WIDGET_GROUP_NAME
        version = (0, 1, 0)

        requires = [PluginRef(group="plugingroup", name="schema", version=(0, 1, 0))]

        plugin_class = Widget
        plugin_info_class = WidgetPlugin

    @overrides
    def check_plugin(self, ep_name: str, plugin: Type[Widget]):
        pg.util.check_implements_method(ep_name, plugin, Widget.show)

    def plugin_deps(self, plugin) -> Set[PluginRef]:
        return set(plugin.Plugin.supports)

    def widgets_for(self, schema: PluginRef) -> Iterator[PluginRef]:
        """Return widgets that support (a parent of) the given schema."""
        ws = set()
        p_path = schemas.parent_path(schema.name, schema.version)
        for s_ref in reversed(p_path):  # in decreasing specifity
            for w_cls in self.values():
                if w_cls.supports(s_ref) and s_ref not in ws:
                    w_ref = w_cls.Plugin.ref()
                    ws.add(w_ref)
                    yield w_ref

widgets_for

widgets_for(schema: PluginRef) -> Iterator[PluginRef]

Return widgets that support (a parent of) the given schema.

Source code in src/metador_core/widget/__init__.py
237
238
239
240
241
242
243
244
245
246
def widgets_for(self, schema: PluginRef) -> Iterator[PluginRef]:
    """Return widgets that support (a parent of) the given schema."""
    ws = set()
    p_path = schemas.parent_path(schema.name, schema.version)
    for s_ref in reversed(p_path):  # in decreasing specifity
        for w_cls in self.values():
            if w_cls.supports(s_ref) and s_ref not in ws:
                w_ref = w_cls.Plugin.ref()
                ws.add(w_ref)
                yield w_ref