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

1"""Common generic widgets. 

2 

3These basically integrate many default widgets provided by panel/bokeh into Metador. 

4""" 

5 

6import json 

7from typing import List, Set, Type 

8 

9import panel as pn 

10from overrides import overrides 

11from panel.viewable import Viewable 

12 

13from metador_core.schema import MetadataSchema 

14 

15from ..plugins import schemas 

16from . import Widget 

17 

18FileMeta = schemas.get("core.file", (0, 1, 0)) 

19ImageFileMeta = schemas.get("core.imagefile", (0, 1, 0)) 

20 

21 

22class FileWidget(Widget): 

23 """Simple widget based on (a subschema of) 'core.file'. 

24 

25 Allows to state supported MIME types with less boilerplate. 

26 """ 

27 

28 class Plugin: 

29 # name and version must be overridden in subclasses 

30 supports = [FileMeta.Plugin.ref()] 

31 

32 MIME_TYPES: Set[str] = set() 

33 """If non-empty, metadata objects must have a MIME type from this set.""" 

34 

35 FILE_EXTS: Set[str] = set() 

36 """If non-empty, filename must have an extension from this set.""" 

37 

38 @property 

39 def title(self) -> str: 

40 return self._meta.name or self._meta.filename or self._node.name 

41 

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 

53 

54 

55# wrap simple generic widgets from panel: 

56 

57# pass content: 

58 

59 

60class MarkdownWidget(FileWidget): 

61 class Plugin(FileWidget.Plugin): 

62 name = "core.file.text.md" 

63 version = (0, 1, 0) 

64 

65 MIME_TYPES = {"text/markdown", "text/x-markdown"} 

66 FILE_EXTS = {".md", ".markdown"} 

67 

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 ) 

73 

74 

75class HTMLWidget(FileWidget): 

76 class Plugin(FileWidget.Plugin): 

77 name = "core.file.text.html" 

78 version = (0, 1, 0) 

79 

80 MIME_TYPES = {"text/html"} 

81 FILE_EXTS = {".htm", ".html"} 

82 

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 ) 

88 

89 

90class CodeWidget(FileWidget): 

91 class Plugin(FileWidget.Plugin): 

92 name = "core.file.text.code" 

93 version = (0, 1, 0) 

94 

95 @classmethod 

96 @overrides 

97 def supports_meta(cls, obj: MetadataSchema) -> bool: 

98 return obj.encodingFormat.startswith("text") 

99 

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 ) 

108 

109 

110class DispatcherWidget(Widget): 

111 """Meta-widget to dispatch a node+metadata object to a more specific widget. 

112 

113 Make sure that the dispatcher widget is probed before the widgets it can 

114 dispatch to. 

115 

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 """ 

120 

121 WIDGETS: List[Type[Widget]] 

122 """Widgets in the order they should be tested.""" 

123 

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 ) 

134 

135 @classmethod 

136 @overrides 

137 def supports_meta(cls, obj: MetadataSchema) -> bool: 

138 return any(map(lambda w: w.supports_meta(obj), cls.WIDGETS)) 

139 

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 

146 

147 @overrides 

148 def show(self) -> Viewable: 

149 return self._widget.show() 

150 

151 

152class TextWidget(DispatcherWidget, FileWidget): 

153 class Plugin(FileWidget.Plugin): 

154 name = "core.file.text" 

155 version = (0, 1, 0) 

156 

157 WIDGETS = [MarkdownWidget, HTMLWidget, CodeWidget] 

158 

159 def show(self) -> Viewable: 

160 return super().show() 

161 

162 

163class JSONWidget(FileWidget): 

164 class Plugin(FileWidget.Plugin): 

165 name = "core.file.json" 

166 version = (0, 1, 0) 

167 

168 MIME_TYPES = {"application/json"} 

169 

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 ) 

180 

181 

182# pass URL: 

183 

184 

185class PDFWidget(FileWidget): 

186 class Plugin(FileWidget.Plugin): 

187 name = "core.file.pdf" 

188 version = (0, 1, 0) 

189 

190 MIME_TYPES = {"application/pdf"} 

191 

192 @overrides 

193 def show(self) -> Viewable: 

194 return pn.pane.PDF(self.file_url(), width=self._w, height=self._h) 

195 

196 

197class ImageWidget(FileWidget): 

198 class Plugin(FileWidget.Plugin): 

199 name = "core.file.image" 

200 version = (0, 1, 0) 

201 

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 } 

209 

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 ) 

218 

219 

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) 

227 

228 MIME_TYPES = {"audio/mpeg", "audio/ogg", "audio/webm"} 

229 

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 ) 

239 

240 

241class VideoWidget(FileWidget): 

242 class Plugin(FileWidget.Plugin): 

243 name = "core.file.video" 

244 version = (0, 1, 0) 

245 

246 MIME_TYPES = {"video/mp4", "video/ogg", "video/webm"} 

247 

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 )