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

131 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-05 13:53 +0000

1# type: ignore 

2"""ImJoy plugin parser module.""" 

3import copy 

4import json 

5import uuid 

6import warnings 

7from pathlib import Path 

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

9from urllib.parse import urljoin 

10 

11import requests 

12from lxml import etree 

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 root = etree.HTML("<html>" + source + "</html>") 

51 plugin_comp = dotdict() 

52 for tag_type in tag_types: 

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

54 values = [] 

55 for elm in elms: 

56 values.append( 

57 dotdict( 

58 attrs=dotdict(elm.attrib), 

59 content=elm.text, 

60 ) 

61 ) 

62 plugin_comp[tag_type] = values 

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

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

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

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

67 else: 

68 raise Exception( 

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

70 ) 

71 

72 overwrite_config = overwrite_config or {} 

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

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

75 ) 

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

77 config["scripts"] = [] 

78 # try to match the script with current tag 

79 for elm in plugin_comp.script: 

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

81 config["script"] = elm.content 

82 # exclude script with mismatched tag 

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

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

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

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

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

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

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

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

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

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

93 

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

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

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

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

98 config["code"] = source 

99 config["id"] = ( 

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

101 ) 

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

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

104 

105 for field in CONFIGURABLE_FIELDS: 

106 obj = config.get(field) 

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

108 if config.get("tag"): 

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

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

111 print( 

112 "WARNING: " 

113 + field 

114 + " do not contain a tag named: " 

115 + config.get("tag") 

116 ) 

117 else: 

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

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

120 return config 

121 

122 

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

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

125 rdf = { 

126 "type": "application", 

127 } 

128 if source_url: 

129 rdf["source"] = source_url 

130 fields = [ 

131 "icon", 

132 "name", 

133 "version", 

134 "api_version", 

135 "description", 

136 "license", 

137 "requirements", 

138 "dependencies", 

139 "env", 

140 "passive", 

141 "services", 

142 ] 

143 for field in fields: 

144 if field in plugin_config: 

145 rdf[field] = plugin_config[field] 

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

147 if "bioengine" not in tags: 

148 tags.append("bioengine") 

149 rdf["tags"] = tags 

150 

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

152 # if isinstance(docs, dict): 

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

154 # elif isinstance(docs, str): 

155 # rdf["documentation"] = docs 

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

157 # make sure we have a list 

158 if not rdf["covers"]: 

159 rdf["covers"] = [] 

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

161 rdf["covers"] = [rdf["covers"]] 

162 

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

164 if not rdf["badges"]: 

165 rdf["badges"] = [] 

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

167 rdf["badges"] = [rdf["badges"]] 

168 

169 authors = plugin_config.get("author") 

170 if authors: 

171 if isinstance(authors, str): 

172 authors = {"name": authors} 

173 if not isinstance(authors, list): 

174 authors = [authors] 

175 rdf["authors"] = authors 

176 

177 return rdf 

178 

179 

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

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

182 req = requests.get(source_url, timeout=5) 

183 source = req.text 

184 plugin_config = parse_imjoy_plugin(source) 

185 rdf = convert_config_to_rdf(plugin_config, source_url) 

186 return rdf 

187 

188 

189def enrich_partial_rdf_with_imjoy_plugin( 

190 partial_rdf: Dict[str, Any], 

191 root: Union[HttpUrl, DirectoryPath], 

192 resolve_rdf_source: Callable[ 

193 [Union[HttpUrl, FilePath, str]], 

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

195 ], 

196) -> Dict[str, Any]: 

197 """ 

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

199 """ 

200 

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

202 if "rdf_source" in partial_rdf: 

203 given_rdf_src = partial_rdf["rdf_source"] 

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

205 ".imjoy.html" 

206 ): 

207 # given_rdf_src is an imjoy plugin 

208 rdf_source = dict(get_plugin_as_rdf(given_rdf_src)) 

209 else: 

210 # given_rdf_src is an actual rdf 

211 if isinstance(given_rdf_src, dict): 

212 rdf_source: Dict[str, Any] = given_rdf_src 

213 else: 

214 try: 

215 rdf_source, _, rdf_source_root = resolve_rdf_source(given_rdf_src) 

216 except Exception as e: 

217 try: 

218 rdf_source, _, rdf_source_root = resolve_rdf_source( 

219 root / given_rdf_src 

220 if isinstance(root, Path) 

221 else urljoin(str(root), given_rdf_src) 

222 ) 

223 except Exception as ee: 

224 rdf_source = {} 

225 warnings.warn( 

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

227 ) 

228 else: 

229 rdf_source["root_path"] = ( 

230 rdf_source_root # enables remote source content to be resolved 

231 ) 

232 else: 

233 rdf_source["root_path"] = ( 

234 rdf_source_root # enables remote source content to be resolved 

235 ) 

236 

237 assert isinstance(rdf_source, dict) 

238 enriched_rdf.update(rdf_source) 

239 

240 if "source" in partial_rdf: 

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

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

243 enriched_rdf.update(rdf_from_source) 

244 

245 enriched_rdf.update( 

246 partial_rdf 

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

248 return enriched_rdf