image_cache
Encrypted, size-capped, on-disk LRU cache of a scan's served image layers.
A served archive is stitched from three independently cached layers — frames,
masks and vectors — each held under its own key: the scheme version, the layer
name, the file's identity (path, mtime, ctime, size) and the conversion
parameters, plus the inference fingerprint and the requested class filter for
the two overlay layers. The frames and masks values are ZIP bytes and the
vectors value is a raw UTF-8 JSON fragment; every value is encrypted at rest
with AES-GCM. The encryption key is derived from the pod's RSA private key (the
same pod_rsa.pem the cache-column encryption uses), with a distinct salt.
Eviction is delegated to diskcache's native LRU + size limit, and the layers are
evicted independently of one another.
Classes
FrameCacheIdentity
class FrameCacheIdentity( path: str, mtime: float, ctime: float, size: int, modality: str | None, laterality: str | None, width: int, quality: int,):Everything that determines a scan's served frames.
Shared by all three layer keys: the frames layer is keyed on this alone, and the overlay layers extend it. Grouping the fields keeps the three builders from drifting apart on which inputs they hash.
Arguments
path: The resolved scan file path.mtime: The file modification time.ctime: The file inode change time (stat.st_ctime).size: The file size in bytes.modality: The selected modality (or None).laterality: The selected laterality (or None).width: The requested output width.quality: The WebP quality.
Variables
- static
ctime : float
- static
laterality : str | None
- static
modality : str | None
- static
mtime : float
- static
path : str
- static
quality : int
- static
size : int
- static
width : int
ScanImageCache
class ScanImageCache(*, cache_dir: Path, size_limit_bytes: int, pod_key_path: Path):On-disk LRU cache of a scan's encrypted, independently keyed image layers.
Arguments
cache_dir: Directory backing the diskcache (per-pod).size_limit_bytes: Max total size before least-recently-used eviction.pod_key_path: Path to the pod'spod_rsa.pem, used to derive the key.
Open the backing diskcache and record the pod key path for derivation.
Static methods
build_frames_key
def build_frames_key(identity: FrameCacheIdentity) ‑> str:Return the key for a scan's served frames.
Carries no segmentation input at all — not the fingerprint, not the selection — so one WebP encode serves every overlay selection, and re-running inference cannot invalidate it.
Arguments
identity: The frame identity.
Returns The frames-layer cache key.
build_masks_key
def build_masks_key( identity: FrameCacheIdentity, *, segmentation_fingerprint: str | None, segmentation_classes: Iterable[str] | None,) ‑> str:Return the key for a scan's rendered mask PNGs.
Arguments
identity: The frame identity.segmentation_fingerprint: Identity of the inference the masks are built from. Included because inference can change while the scan file does not (a re-run at a new model version), so the file stat alone would leave a warm entry serving stale masks. Empty when the file carries no inference rows at all, which is still an identity: it partitions "nothing has run yet" away from every result.segmentation_classes: The requested class filter, or None for every available class.
Returns The masks-layer cache key.
build_vectors_key
def build_vectors_key( identity: FrameCacheIdentity, *, segmentation_fingerprint: str | None, segmentation_classes: Iterable[str] | None,) ‑> str:Return the key for a scan's raw segmentation vectors.
Keyed on the same material as the masks layer, under a different layer name: the two outputs come from one inference and one class selection, but they are stored apart so a vectors-only request never pays for a PNG encode and a masks-only request never pays to collect instances.
Arguments
identity: The frame identity.segmentation_fingerprint: Identity of the inference the vectors come from. None when overlay serving is unavailable.segmentation_classes: The requested class filter, or None for every available class.
Returns The vectors-layer cache key.
Methods
delete
def delete(self, key: str) ‑> None:Drop an entry, best-effort, so the next request for that key rebuilds.
For an entry a caller has found unusable for a reason its key does not encode: the key would otherwise keep serving it until eviction.
Invalidation is advisory, and the guarantee belongs to the operation
rather than to each call site. A caller reaches here having already
decided not to serve the entry, so a failed drop changes nothing it can
act on — the only consequence is that the next request retries the drop.
Raising instead would let a recovery step become the reason a request
fails, which is how the code path that exists to degrade gracefully ends
up returning nothing at all. A contended lock is retried first (see
_retrying), so what reaches this catch is contention that outlasted
every attempt, or a fault of another kind.
This catches broadly where peek narrows, and the asymmetry is
deliberate: a read that swallowed everything would report a systemic
failure as a miss and rebuild every entry on every request, while a
failed drop of an entry already known to be unusable costs nothing
beyond a warning.
Arguments
key: The cache key. An absent key is a no-op.
get_or_build
def get_or_build( self, key: str, builder: Callable[[], bytes], *, refresh: bool = False,) ‑> bytes:Return cached plaintext bytes for key, building + storing on miss.
Arguments
key: The cache key (see thebuild_*_keybuilders).builder: Zero-arg callable producing the plaintext bytes on a miss.refresh: When True, drop any cached entry up front and rebuild.
Returns The plaintext bytes. Storing them is best-effort: by the time the store is written the bytes are already built, so a failure there costs the next request a rebuild and costs this one nothing.
Raises
diskcache.Timeout: Propagated from the read alone, and only when the store's lock stayed contended across every retry. A read that reported contention as a miss would put every concurrent request through a full scan decode it then failed to store.
peek
def peek(self, key: str) ‑> bytes | None:Return a cached entry's plaintext, or None on a miss.
Arguments
key: The cache key.
Returns The plaintext bytes, or None when the entry is absent or unreadable.
A corrupt/truncated entry reads as a miss so the caller rebuilds it:
a bad AES-GCM tag surfaces as DecryptError (via
_AESEncryption.decrypt), a short blob as ValueError (nonce too
short). Any non-bytes stored value is treated the same way, as
corrupt, without being handed to AES-GCM.
The catch covers those cases alone. Anything else — a key-derivation
regression raising RuntimeError, say — leaves this method rather
than being reported as a miss, so a systemic failure is never read
as "rebuild this one entry". What it then costs is the caller's
decision, not this method's: the overlay path, for one, catches per
layer so a read failure costs that layer alone.
Raises
diskcache.Timeout: The store's lock was still contended after_LOCK_RETRIESattempts. Deliberately not reported as a miss: under sustained contention every request would then rebuild a warm entry — a full scan decode each — and fail to store it.
put
def put(self, key: str, plaintext: bytes) ‑> None:Encrypt and store an entry.
Arguments
key: The cache key.plaintext: The bytes to store.
Raises
diskcache.Timeout: The store's lock was still contended after_LOCK_RETRIESattempts. Left to the caller, which holds the built value:get_or_buildand the overlay layers serve it and log rather than discarding a rendered payload.