Coverage for src/bioimageio/spec/_internal/io_basics.py: 96%
94 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
1from __future__ import annotations
3import hashlib
4import os
5import zipfile
6from contextlib import nullcontext
7from functools import partial
8from pathlib import Path
9from typing import Any, ClassVar, Protocol, runtime_checkable
10from zipfile import ZipFile
12import pydantic
13import zipp # pyright: ignore[reportMissingTypeStubs]
14from annotated_types import Predicate
15from pydantic import RootModel, StringConstraints
16from typing_extensions import Annotated
18from .root_url import RootHttpUrl
19from .validated_string import ValidatedString
21FileName = str
22FilePath = Annotated[pydantic.FilePath, pydantic.Field(title="FilePath")]
23AbsoluteDirectory = Annotated[
24 pydantic.DirectoryPath,
25 Predicate(Path.is_absolute),
26 pydantic.Field(title="AbsoluteDirectory"),
27]
28AbsoluteFilePath = Annotated[
29 pydantic.FilePath,
30 Predicate(Path.is_absolute),
31 pydantic.Field(title="AbsoluteFilePath"),
32]
34BIOIMAGEIO_YAML = "rdf.yaml"
35ALTERNATIVE_BIOIMAGEIO_YAML_NAMES = ("bioimageio.yaml", "model.yaml")
36ALL_BIOIMAGEIO_YAML_NAMES = (BIOIMAGEIO_YAML,) + ALTERNATIVE_BIOIMAGEIO_YAML_NAMES
38ZipPath = zipp.Path # not zipfile.Path due to https://bugs.python.org/issue40564
41class Sha256(ValidatedString):
42 """A SHA-256 hash value"""
44 root_model: ClassVar[type[RootModel[Any]]] = RootModel[
45 Annotated[
46 str,
47 StringConstraints(
48 strip_whitespace=True, to_lower=True, min_length=64, max_length=64
49 ),
50 ]
51 ]
54class BytesReaderP(Protocol):
55 def read(self, size: int = -1, /) -> bytes: ...
57 @property
58 def closed(self) -> bool: ...
60 def readable(self) -> bool: ...
62 def seek(self, offset: int, whence: int = os.SEEK_SET, /) -> int: ...
64 def seekable(self) -> bool: ...
66 def tell(self) -> int: ...
69@runtime_checkable
70class BytesReaderIntoP(BytesReaderP, Protocol):
71 def readinto(self, b: bytearray | memoryview) -> int: ...
74Suffix = str
77class BytesReader(BytesReaderP):
78 def __init__(
79 self,
80 /,
81 reader: BytesReaderP | BytesReaderIntoP,
82 *,
83 sha256: Sha256 | None,
84 suffix: Suffix,
85 original_file_name: FileName,
86 original_root: RootHttpUrl | AbsoluteDirectory | ZipFile,
87 is_zipfile: bool | None,
88 ) -> None:
89 self._reader = reader
90 self._sha256 = sha256
91 self._suffix = suffix
92 self._original_file_name = original_file_name
93 self._original_root = original_root
94 self._is_zipfile = is_zipfile
95 super().__init__()
97 @property
98 def is_zipfile(self) -> bool:
99 if self._is_zipfile is None:
100 pos = self.tell()
101 self._is_zipfile = zipfile.is_zipfile(self)
102 _ = self.seek(pos)
104 return self._is_zipfile
106 @property
107 def sha256(self) -> Sha256:
108 if self._sha256 is None:
109 pos = self._reader.tell()
110 _ = self._reader.seek(0)
111 self._sha256 = get_sha256(self._reader)
112 _ = self._reader.seek(pos)
114 return self._sha256
116 @property
117 def suffix(self) -> Suffix:
118 return self._suffix
120 @property
121 def original_file_name(self) -> FileName:
122 return self._original_file_name
124 @property
125 def original_root(self) -> RootHttpUrl | AbsoluteDirectory | ZipFile:
126 return self._original_root
128 def read(self, size: int = -1, /) -> bytes:
129 return self._reader.read(size)
131 def read_text(self, encoding: str = "utf-8") -> str:
132 return self._reader.read().decode(encoding)
134 def readable(self) -> bool:
135 return True
137 def seek(self, offset: int, whence: int = os.SEEK_SET, /) -> int:
138 return self._reader.seek(offset, whence)
140 def seekable(self) -> bool:
141 return True
143 def tell(self) -> int:
144 return self._reader.tell()
146 @property
147 def closed(self) -> bool:
148 return self._reader.closed
151def get_sha256(source: BytesReaderP | BytesReaderIntoP | Path) -> Sha256:
152 chunksize = 128 * 1024
153 h = hashlib.sha256()
155 if isinstance(source, BytesReaderIntoP):
156 b = bytearray(chunksize)
157 mv = memoryview(b)
158 for n in iter(lambda: source.readinto(mv), 0):
159 h.update(mv[:n])
160 else:
161 if isinstance(source, Path):
162 read_ctxt = source.open(mode="rb")
163 else:
164 read_ctxt = nullcontext(source)
166 with read_ctxt as r:
167 for chunk in iter(partial(r.read, chunksize), b""):
168 h.update(chunk)
170 sha = h.hexdigest()
171 return Sha256(sha)