Coverage for src/metador_core/widget/common.py: 80%
124 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-08 10:29 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-08 10:29 +0000
1"""Common generic widgets.
3These basically integrate many default widgets provided by panel/bokeh into Metador.
4"""
6import json
7from typing import List, Set, Type
9import panel as pn
10from overrides import overrides
11from panel.viewable import Viewable
13from metador_core.schema import MetadataSchema
15from ..plugins import schemas
16from . import Widget
18FileMeta = schemas.get("core.file", (0, 1, 0))
19ImageFileMeta = schemas.get("core.imagefile", (0, 1, 0))
22class FileWidget(Widget):
23 """Simple widget based on (a subschema of) 'core.file'.
25 Allows to state supported MIME types with less boilerplate.
26 """
28 class Plugin:
29 # name and version must be overridden in subclasses
30 supports = [FileMeta.Plugin.ref()]
32 MIME_TYPES: Set[str] = set()
33 """If non-empty, metadata objects must have a MIME type from this set."""
35 FILE_EXTS: Set[str] = set()
36 """If non-empty, filename must have an extension from this set."""
38 @property
39 def title(self) -> str:
40 return self._meta.name or self._meta.filename or self._node.name
42 @classmethod
43 @overrides
44 def supports_meta(cls, obj: MetadataSchema) -> bool:
45 supported_mime = True
46 if cls.MIME_TYPES:
47 supported_mime = obj.encodingFormat in cls.MIME_TYPES
48 supported_ext = False
49 if cls.FILE_EXTS:
50 supported_ext = obj.filename.endswith(tuple(cls.FILE_EXTS))
51 # either supported mime or supported ext is enough
52 return supported_mime or supported_ext
55# wrap simple generic widgets from panel:
57# pass content:
60class MarkdownWidget(FileWidget):
61 class Plugin(FileWidget.Plugin):
62 name = "core.file.text.md"
63 version = (0, 1, 0)
65 MIME_TYPES = {"text/markdown", "text/x-markdown"}
66 FILE_EXTS = {".md", ".markdown"}
68 @overrides
69 def show(self) -> Viewable:
70 return pn.pane.Markdown(
71 self.file_data().decode("utf-8"), max_width=self._w, max_height=self._h
72 )
75class HTMLWidget(FileWidget):
76 class Plugin(FileWidget.Plugin):
77 name = "core.file.text.html"
78 version = (0, 1, 0)
80 MIME_TYPES = {"text/html"}
81 FILE_EXTS = {".htm", ".html"}
83 @overrides
84 def show(self) -> Viewable:
85 return pn.pane.HTML(
86 self.file_data().decode("utf-8"), max_width=self._w, max_height=self._h
87 )
90class CodeWidget(FileWidget):
91 class Plugin(FileWidget.Plugin):
92 name = "core.file.text.code"
93 version = (0, 1, 0)
95 @classmethod
96 @overrides
97 def supports_meta(cls, obj: MetadataSchema) -> bool:
98 return obj.encodingFormat.startswith("text")
100 def show(self) -> Viewable:
101 return pn.widgets.Ace(
102 filename=self._meta.filename,
103 readonly=True,
104 value=self.file_data().decode("utf-8"),
105 width=self._w,
106 height=self._h,
107 )
110class DispatcherWidget(Widget):
111 """Meta-widget to dispatch a node+metadata object to a more specific widget.
113 Make sure that the dispatcher widget is probed before the widgets it can
114 dispatch to.
116 This works if and only if for each widget in listed in `WIDGETS`:
117 * the plugin name of the dispatcher is a prefix of the widget name, or
118 * the widget has `primary = False` and thus is not considered by the dashboard.
119 """
121 WIDGETS: List[Type[Widget]]
122 """Widgets in the order they should be tested."""
124 def dispatch(self, w_cls: Type[Widget]) -> Widget:
125 """Dispatch to another widget (used by meta-widgets)."""
126 return w_cls(
127 self._node,
128 "",
129 server=self._server,
130 metadata=self._meta,
131 max_width=self._w,
132 max_height=self._h,
133 )
135 @classmethod
136 @overrides
137 def supports_meta(cls, obj: MetadataSchema) -> bool:
138 return any(map(lambda w: w.supports_meta(obj), cls.WIDGETS))
140 @overrides
141 def setup(self):
142 for w_cls in self.WIDGETS:
143 if w_cls.supports_meta(self._meta):
144 self._widget = self.dispatch(w_cls)
145 break
147 @overrides
148 def show(self) -> Viewable:
149 return self._widget.show()
152class TextWidget(DispatcherWidget, FileWidget):
153 class Plugin(FileWidget.Plugin):
154 name = "core.file.text"
155 version = (0, 1, 0)
157 WIDGETS = [MarkdownWidget, HTMLWidget, CodeWidget]
159 def show(self) -> Viewable:
160 return super().show()
163class JSONWidget(FileWidget):
164 class Plugin(FileWidget.Plugin):
165 name = "core.file.json"
166 version = (0, 1, 0)
168 MIME_TYPES = {"application/json"}
170 @overrides
171 def show(self) -> Viewable:
172 return pn.pane.JSON(
173 json.loads(self.file_data()),
174 name=self.title,
175 max_width=self._w,
176 max_height=self._h,
177 hover_preview=True,
178 depth=-1,
179 )
182# pass URL:
185class PDFWidget(FileWidget):
186 class Plugin(FileWidget.Plugin):
187 name = "core.file.pdf"
188 version = (0, 1, 0)
190 MIME_TYPES = {"application/pdf"}
192 @overrides
193 def show(self) -> Viewable:
194 return pn.pane.PDF(self.file_url(), width=self._w, height=self._h)
197class ImageWidget(FileWidget):
198 class Plugin(FileWidget.Plugin):
199 name = "core.file.image"
200 version = (0, 1, 0)
202 MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/svg+xml"}
203 PANEL_WIDGET = {
204 "image/jpeg": pn.pane.JPG,
205 "image/png": pn.pane.PNG,
206 "image/gif": pn.pane.GIF,
207 "image/svg+xml": pn.pane.SVG,
208 }
210 @overrides
211 def show(self) -> Viewable:
212 return self.PANEL_WIDGET[self._meta.encodingFormat](
213 self.file_url(),
214 width=self._w,
215 height=self._h,
216 alt_text="Sorry. Something went wrong while loading the resource",
217 )
220# NOTE: Panel based widget does not work.
221# see - https://github.com/holoviz/panel/issues/3203
222# using HTML based audio & video panes for the audio and video widgets respectively
223class AudioWidget(FileWidget):
224 class Plugin(FileWidget.Plugin):
225 name = "core.file.audio"
226 version = (0, 1, 0)
228 MIME_TYPES = {"audio/mpeg", "audio/ogg", "audio/webm"}
230 @overrides
231 def show(self) -> Viewable:
232 return pn.pane.HTML(
233 f"""
234 <audio controls>
235 <source src={self.file_url()} type={self._meta.encodingFormat}>
236 </audio>
237 """,
238 )
241class VideoWidget(FileWidget):
242 class Plugin(FileWidget.Plugin):
243 name = "core.file.video"
244 version = (0, 1, 0)
246 MIME_TYPES = {"video/mp4", "video/ogg", "video/webm"}
248 @overrides
249 def show(self) -> Viewable:
250 return pn.pane.HTML(
251 f"""
252 <video width={self._w} height={self._h} controls style="object-position: center; object-fit:cover;">
253 <source src={self.file_url()} type={self._meta.encodingFormat}>
254 </video>
255 """,
256 )