Coverage for bioimageio/spec/partner_utils/imjoy/_plugin_parser.py: 17%

131 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 09:20 +0000

1# pragma: no cover 

2# type: ignore 

3"""ImJoy plugin parser module.""" 

4import copy 

5import json 

6import uuid 

7import warnings 

8from pathlib import Path 

9from typing import Any, Callable, Dict, Tuple, Union 

10from urllib.parse import urljoin 

11 

12import httpx 

13from pydantic import DirectoryPath, FilePath, HttpUrl 

14from ruyaml import YAML 

15 

16yaml = YAML(typ="safe") 

17 

18tag_types = ["config", "script", "link", "window", "style", "docs", "attachment"] 

19 

20CONFIGURABLE_FIELDS = [ 

21 "env", 

22 "requirements", 

23 "dependencies", 

24 "icon", 

25 "ui", 

26 "type", 

27 "flags", 

28 "labels", 

29 "cover", 

30 "base_frame", 

31 "base_worker", 

32 "passive", 

33] 

34 

35 

36class dotdict(dict): # pylint: disable=invalid-name 

37 """Access dictionary attributes with dot.notation.""" 

38 

39 __getattr__ = dict.get 

40 __setattr__ = dict.__setitem__ 

41 __delattr__ = dict.__delitem__ 

42 

43 def __deepcopy__(self, memo=None): 

44 """Make a deep copy.""" 

45 return dotdict(copy.deepcopy(dict(self), memo=memo)) 

46 

47 

48def parse_imjoy_plugin(source, overwrite_config=None): 

49 """Parse ImJoy plugin file and return a dict with all the fields.""" 

50 from lxml import etree 

51 

52 root = etree.HTML("<html>" + source + "</html>") 

53 plugin_comp = dotdict() 

54 for tag_type in tag_types: 

55 elms = root.xpath(f".//{tag_type}") 

56 values = [] 

57 for elm in elms: 

58 values.append( 

59 dotdict( 

60 attrs=dotdict(elm.attrib), 

61 content=elm.text, 

62 ) 

63 ) 

64 plugin_comp[tag_type] = values 

65 if plugin_comp.config[0].attrs.lang == "yaml": 

66 config = yaml.load(plugin_comp.config[0].content) 

67 elif plugin_comp.config[0].attrs.lang == "json": 

68 config = json.loads(plugin_comp.config[0].content) 

69 else: 

70 raise Exception( 

71 "Unsupported config language: " + plugin_comp.config[0].attrs.lang 

72 ) 

73 

74 overwrite_config = overwrite_config or {} 

75 config["tag"] = overwrite_config.get("tag") or ( 

76 config.get("tags") and config.get("tags")[0] 

77 ) 

78 config["hot_reloading"] = overwrite_config.get("hot_reloading") 

79 config["scripts"] = [] 

80 # try to match the script with current tag 

81 for elm in plugin_comp.script: 

82 if elm.attrs.tag == config["tag"]: 

83 config["script"] = elm.content 

84 # exclude script with mismatched tag 

85 if not elm.attrs.tag or elm.attrs.tag == config["tag"]: 

86 config["scripts"].append(elm) 

87 if not config.get("script") and len(plugin_comp.script) > 0: 

88 config["script"] = plugin_comp.script[0].content 

89 config["lang"] = plugin_comp.script[0].attrs.lang 

90 config["links"] = plugin_comp.link or None 

91 config["windows"] = plugin_comp.window or None 

92 config["styles"] = plugin_comp.style or None 

93 config["docs"] = plugin_comp.docs[0] if plugin_comp.docs else config.get("docs") 

94 config["attachments"] = plugin_comp.attachment or None 

95 

96 config["_id"] = overwrite_config.get("_id") or config.get("name").replace(" ", "_") 

97 config["uri"] = overwrite_config.get("uri") 

98 config["origin"] = overwrite_config.get("origin") 

99 config["namespace"] = overwrite_config.get("namespace") 

100 config["code"] = source 

101 config["id"] = ( 

102 config.get("name").strip().replace(" ", "_") + "_" + str(uuid.uuid4()) 

103 ) 

104 config["runnable"] = config.get("runnable", True) 

105 config["requirements"] = config.get("requirements") or [] 

106 

107 for field in CONFIGURABLE_FIELDS: 

108 obj = config.get(field) 

109 if obj and isinstance(obj, dict) and not isinstance(obj, list): 

110 if config.get("tag"): 

111 config[field] = obj.get(config.get("tag")) 

112 if not obj.get(config.get("tag")): 

113 print( 

114 "WARNING: " 

115 + field 

116 + " do not contain a tag named: " 

117 + config.get("tag") 

118 ) 

119 else: 

120 raise Exception("You must use 'tags' with configurable fields.") 

121 config["lang"] = config.get("lang") or "javascript" 

122 return config 

123 

124 

125def convert_config_to_rdf(plugin_config, source_url=None) -> dict: 

126 """Convert imjoy plugin config to RDF format.""" 

127 rdf = { 

128 "type": "application", 

129 } 

130 if source_url: 

131 rdf["source"] = source_url 

132 fields = [ 

133 "icon", 

134 "name", 

135 "version", 

136 "api_version", 

137 "description", 

138 "license", 

139 "requirements", 

140 "dependencies", 

141 "env", 

142 "passive", 

143 "services", 

144 ] 

145 for field in fields: 

146 if field in plugin_config: 

147 rdf[field] = plugin_config[field] 

148 tags = plugin_config.get("labels", []) + plugin_config.get("flags", []) 

149 if "bioengine" not in tags: 

150 tags.append("bioengine") 

151 rdf["tags"] = tags 

152 

153 # docs = plugin_config.get("docs") 

154 # if isinstance(docs, dict): 

155 # rdf["documentation"] = docs.get("content") 

156 # elif isinstance(docs, str): 

157 # rdf["documentation"] = docs 

158 rdf["covers"] = plugin_config.get("cover") 

159 # make sure we have a list 

160 if not rdf["covers"]: 

161 rdf["covers"] = [] 

162 elif not isinstance(rdf["covers"], list): 

163 rdf["covers"] = [rdf["covers"]] 

164 

165 rdf["badges"] = plugin_config.get("badge") 

166 if not rdf["badges"]: 

167 rdf["badges"] = [] 

168 elif not isinstance(rdf["badges"], list): 

169 rdf["badges"] = [rdf["badges"]] 

170 

171 authors = plugin_config.get("author") 

172 if authors: 

173 if isinstance(authors, str): 

174 authors = {"name": authors} 

175 if not isinstance(authors, list): 

176 authors = [authors] 

177 rdf["authors"] = authors 

178 

179 return rdf 

180 

181 

182def get_plugin_as_rdf(source_url: str) -> Dict[Any, Any]: 

183 """Get imjoy plugin config in RDF format.""" 

184 req = httpx.get(source_url, timeout=5) 

185 source = req.text 

186 plugin_config = parse_imjoy_plugin(source) 

187 rdf = convert_config_to_rdf(plugin_config, source_url) 

188 return rdf 

189 

190 

191def enrich_partial_rdf_with_imjoy_plugin( 

192 partial_rdf: Dict[str, Any], 

193 root: Union[HttpUrl, DirectoryPath], 

194 resolve_rdf_source: Callable[ 

195 [Union[HttpUrl, FilePath, str]], 

196 Tuple[Dict[str, Any], str, Union[HttpUrl, DirectoryPath]], 

197 ], 

198) -> Dict[str, Any]: 

199 """ 

200 a (partial) rdf may have 'rdf_source' or 'source' which resolve to rdf data that may be overwritten. 

201 """ 

202 

203 enriched_rdf: Dict[str, Any] = {} 

204 if "rdf_source" in partial_rdf: 

205 given_rdf_src = partial_rdf["rdf_source"] 

206 if isinstance(given_rdf_src, str) and given_rdf_src.split("?")[0].endswith( 

207 ".imjoy.html" 

208 ): 

209 # given_rdf_src is an imjoy plugin 

210 rdf_source = dict(get_plugin_as_rdf(given_rdf_src)) 

211 else: 

212 # given_rdf_src is an actual rdf 

213 if isinstance(given_rdf_src, dict): 

214 rdf_source: Dict[str, Any] = given_rdf_src 

215 else: 

216 try: 

217 rdf_source, _, rdf_source_root = resolve_rdf_source(given_rdf_src) 

218 except Exception as e: 

219 try: 

220 rdf_source, _, rdf_source_root = resolve_rdf_source( 

221 root / given_rdf_src 

222 if isinstance(root, Path) 

223 else urljoin(str(root), given_rdf_src) 

224 ) 

225 except Exception as ee: 

226 rdf_source = {} 

227 warnings.warn( 

228 f"Failed to resolve `rdf_source`: 1. {e}\n2. {ee}" 

229 ) 

230 else: 

231 rdf_source["root_path"] = ( 

232 rdf_source_root # enables remote source content to be resolved 

233 ) 

234 else: 

235 rdf_source["root_path"] = ( 

236 rdf_source_root # enables remote source content to be resolved 

237 ) 

238 

239 assert isinstance(rdf_source, dict) 

240 enriched_rdf.update(rdf_source) 

241 

242 if "source" in partial_rdf: 

243 if partial_rdf["source"].split("?")[0].endswith(".imjoy.html"): 

244 rdf_from_source = get_plugin_as_rdf(partial_rdf["source"]) 

245 enriched_rdf.update(rdf_from_source) 

246 

247 enriched_rdf.update( 

248 partial_rdf 

249 ) # initial partial rdf overwrites fields from rdf_source or source 

250 return enriched_rdf