Coverage for bioimageio/spec/_package.py: 81%

93 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-11 07:34 +0000

1import collections.abc 

2import shutil 

3from io import BytesIO 

4from pathlib import Path 

5from tempfile import NamedTemporaryFile, mkdtemp 

6from typing import IO, Dict, Literal, Optional, Sequence, Union 

7from zipfile import ZIP_DEFLATED 

8 

9from loguru import logger 

10from pydantic import DirectoryPath, FilePath, NewPath 

11 

12from ._description import InvalidDescr, ResourceDescr, build_description 

13from ._internal.common_nodes import ResourceDescrBase 

14from ._internal.io import ( 

15 BioimageioYamlContent, 

16 BioimageioYamlSource, 

17 FileDescr, 

18 RelativeFilePath, 

19 ensure_is_valid_bioimageio_yaml_name, 

20) 

21from ._internal.io_basics import ( 

22 BIOIMAGEIO_YAML, 

23 AbsoluteFilePath, 

24 BytesReader, 

25 FileName, 

26 ZipPath, 

27) 

28from ._internal.io_utils import open_bioimageio_yaml, write_yaml, write_zip 

29from ._internal.packaging_context import PackagingContext 

30from ._internal.url import HttpUrl 

31from ._internal.utils import get_os_friendly_file_name 

32from ._internal.validation_context import get_validation_context 

33from ._internal.warning_levels import ERROR 

34from ._io import load_description 

35from .model.v0_4 import WeightsFormat 

36 

37 

38# TODO: deprecate in favor of get_package_content 

39def get_resource_package_content( 

40 rd: ResourceDescr, 

41 /, 

42 *, 

43 bioimageio_yaml_file_name: FileName = BIOIMAGEIO_YAML, 

44 weights_priority_order: Optional[Sequence[WeightsFormat]] = None, # model only 

45) -> Dict[FileName, Union[HttpUrl, AbsoluteFilePath, BioimageioYamlContent, ZipPath]]: 

46 ret: Dict[ 

47 FileName, Union[HttpUrl, AbsoluteFilePath, BioimageioYamlContent, ZipPath] 

48 ] = {} 

49 for k, v in get_package_content( 

50 rd, 

51 bioimageio_yaml_file_name=bioimageio_yaml_file_name, 

52 weights_priority_order=weights_priority_order, 

53 ).items(): 

54 if isinstance(v, FileDescr): 

55 if isinstance(v.source, (Path, RelativeFilePath)): 

56 ret[k] = v.source.absolute() 

57 else: 

58 ret[k] = v.source 

59 

60 else: 

61 ret[k] = v 

62 

63 return ret 

64 

65 

66def get_package_content( 

67 rd: ResourceDescr, 

68 /, 

69 *, 

70 bioimageio_yaml_file_name: FileName = BIOIMAGEIO_YAML, 

71 weights_priority_order: Optional[Sequence[WeightsFormat]] = None, # model only 

72) -> Dict[FileName, Union[FileDescr, BioimageioYamlContent]]: 

73 """ 

74 Args: 

75 rd: resource description 

76 bioimageio_yaml_file_name: RDF file name 

77 # for model resources only: 

78 weights_priority_order: If given, only the first weights format present in the model is included. 

79 If none of the prioritized weights formats is found a ValueError is raised. 

80 """ 

81 os_friendly_name = get_os_friendly_file_name(rd.name) 

82 bioimageio_yaml_file_name = bioimageio_yaml_file_name.format( 

83 name=os_friendly_name, type=rd.type 

84 ) 

85 

86 bioimageio_yaml_file_name = ensure_is_valid_bioimageio_yaml_name( 

87 bioimageio_yaml_file_name 

88 ) 

89 content: Dict[FileName, FileDescr] = {} 

90 with PackagingContext( 

91 bioimageio_yaml_file_name=bioimageio_yaml_file_name, 

92 file_sources=content, 

93 weights_priority_order=weights_priority_order, 

94 ): 

95 rdf_content: BioimageioYamlContent = rd.model_dump( 

96 mode="json", exclude_unset=True 

97 ) 

98 

99 _ = rdf_content.pop("rdf_source", None) 

100 

101 return {**content, bioimageio_yaml_file_name: rdf_content} 

102 

103 

104def _prepare_resource_package( 

105 source: Union[BioimageioYamlSource, ResourceDescr], 

106 /, 

107 *, 

108 weights_priority_order: Optional[Sequence[WeightsFormat]] = None, 

109) -> Dict[FileName, Union[BioimageioYamlContent, BytesReader]]: 

110 """Prepare to package a resource description; downloads all required files. 

111 

112 Args: 

113 source: A bioimage.io resource description (as file, raw YAML content or description class) 

114 context: validation context 

115 weights_priority_order: If given only the first weights format present in the model is included. 

116 If none of the prioritized weights formats is found all are included. 

117 """ 

118 context = get_validation_context() 

119 bioimageio_yaml_file_name = context.file_name 

120 if isinstance(source, ResourceDescrBase): 

121 descr = source 

122 elif isinstance(source, collections.abc.Mapping): 

123 descr = build_description(source) 

124 else: 

125 opened = open_bioimageio_yaml(source) 

126 bioimageio_yaml_file_name = opened.original_file_name 

127 context = context.replace( 

128 root=opened.original_root, file_name=opened.original_file_name 

129 ) 

130 with context: 

131 descr = build_description(opened.content) 

132 

133 if isinstance(descr, InvalidDescr): 

134 raise ValueError(f"{source} is invalid: {descr.validation_summary}") 

135 

136 with context: 

137 package_content = get_package_content( 

138 descr, 

139 bioimageio_yaml_file_name=bioimageio_yaml_file_name or BIOIMAGEIO_YAML, 

140 weights_priority_order=weights_priority_order, 

141 ) 

142 

143 return { 

144 k: v if isinstance(v, collections.abc.Mapping) else v.get_reader() 

145 for k, v in package_content.items() 

146 } 

147 

148 

149def save_bioimageio_package_as_folder( 

150 source: Union[BioimageioYamlSource, ResourceDescr], 

151 /, 

152 *, 

153 output_path: Union[NewPath, DirectoryPath, None] = None, 

154 weights_priority_order: Optional[ # model only 

155 Sequence[ 

156 Literal[ 

157 "keras_hdf5", 

158 "onnx", 

159 "pytorch_state_dict", 

160 "tensorflow_js", 

161 "tensorflow_saved_model_bundle", 

162 "torchscript", 

163 ] 

164 ] 

165 ] = None, 

166) -> DirectoryPath: 

167 """Write the content of a bioimage.io resource package to a folder. 

168 

169 Args: 

170 source: bioimageio resource description 

171 output_path: file path to write package to 

172 weights_priority_order: If given only the first weights format present in the model is included. 

173 If none of the prioritized weights formats is found all are included. 

174 

175 Returns: 

176 directory path to bioimageio package folder 

177 """ 

178 package_content = _prepare_resource_package( 

179 source, 

180 weights_priority_order=weights_priority_order, 

181 ) 

182 if output_path is None: 

183 output_path = Path(mkdtemp()) 

184 else: 

185 output_path = Path(output_path) 

186 

187 output_path.mkdir(exist_ok=True, parents=True) 

188 for name, src in package_content.items(): 

189 if isinstance(src, collections.abc.Mapping): 

190 write_yaml(src, output_path / name) 

191 elif ( 

192 isinstance(src.original_root, Path) 

193 and src.original_root / src.original_file_name 

194 == (output_path / name).resolve() 

195 ): 

196 logger.debug( 

197 f"Not copying {src.original_root / src.original_file_name} to itself." 

198 ) 

199 else: 

200 if isinstance(src.original_root, Path): 

201 logger.debug( 

202 f"Copying from path {src.original_root / src.original_file_name} to {output_path / name}." 

203 ) 

204 else: 

205 logger.debug( 

206 f"Copying {src.original_root}/{src.original_file_name} to {output_path / name}." 

207 ) 

208 with (output_path / name).open("wb") as dest: 

209 _ = shutil.copyfileobj(src, dest) 

210 

211 return output_path 

212 

213 

214def save_bioimageio_package( 

215 source: Union[BioimageioYamlSource, ResourceDescr], 

216 /, 

217 *, 

218 compression: int = ZIP_DEFLATED, 

219 compression_level: int = 1, 

220 output_path: Union[NewPath, FilePath, None] = None, 

221 weights_priority_order: Optional[ # model only 

222 Sequence[ 

223 Literal[ 

224 "keras_hdf5", 

225 "onnx", 

226 "pytorch_state_dict", 

227 "tensorflow_js", 

228 "tensorflow_saved_model_bundle", 

229 "torchscript", 

230 ] 

231 ] 

232 ] = None, 

233 allow_invalid: bool = False, 

234) -> FilePath: 

235 """Package a bioimageio resource as a zip file. 

236 

237 Args: 

238 rd: bioimageio resource description 

239 compression: The numeric constant of compression method. 

240 compression_level: Compression level to use when writing files to the archive. 

241 See https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile 

242 output_path: file path to write package to 

243 weights_priority_order: If given only the first weights format present in the model is included. 

244 If none of the prioritized weights formats is found all are included. 

245 

246 Returns: 

247 path to zipped bioimageio package 

248 """ 

249 package_content = _prepare_resource_package( 

250 source, 

251 weights_priority_order=weights_priority_order, 

252 ) 

253 if output_path is None: 

254 output_path = Path( 

255 NamedTemporaryFile(suffix=".bioimageio.zip", delete=False).name 

256 ) 

257 else: 

258 output_path = Path(output_path) 

259 

260 write_zip( 

261 output_path, 

262 package_content, 

263 compression=compression, 

264 compression_level=compression_level, 

265 ) 

266 with get_validation_context().replace(warning_level=ERROR): 

267 if isinstance((exported := load_description(output_path)), InvalidDescr): 

268 exported.validation_summary.display() 

269 msg = f"Exported package at '{output_path}' is invalid." 

270 if allow_invalid: 

271 logger.error(msg) 

272 else: 

273 raise ValueError(msg) 

274 

275 return output_path 

276 

277 

278def save_bioimageio_package_to_stream( 

279 source: Union[BioimageioYamlSource, ResourceDescr], 

280 /, 

281 *, 

282 compression: int = ZIP_DEFLATED, 

283 compression_level: int = 1, 

284 output_stream: Union[IO[bytes], None] = None, 

285 weights_priority_order: Optional[ # model only 

286 Sequence[ 

287 Literal[ 

288 "keras_hdf5", 

289 "onnx", 

290 "pytorch_state_dict", 

291 "tensorflow_js", 

292 "tensorflow_saved_model_bundle", 

293 "torchscript", 

294 ] 

295 ] 

296 ] = None, 

297) -> IO[bytes]: 

298 """Package a bioimageio resource into a stream. 

299 

300 Args: 

301 rd: bioimageio resource description 

302 compression: The numeric constant of compression method. 

303 compression_level: Compression level to use when writing files to the archive. 

304 See https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile 

305 output_stream: stream to write package to 

306 weights_priority_order: If given only the first weights format present in the model is included. 

307 If none of the prioritized weights formats is found all are included. 

308 

309 Note: this function bypasses safety checks and does not load/validate the model after writing. 

310 

311 Returns: 

312 stream of zipped bioimageio package 

313 """ 

314 if output_stream is None: 

315 output_stream = BytesIO() 

316 

317 package_content = _prepare_resource_package( 

318 source, 

319 weights_priority_order=weights_priority_order, 

320 ) 

321 

322 write_zip( 

323 output_stream, 

324 package_content, 

325 compression=compression, 

326 compression_level=compression_level, 

327 ) 

328 

329 return output_stream