micro-reader 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {micro_reader-0.0.1 → micro_reader-0.0.2}/PKG-INFO +18 -2
  2. {micro_reader-0.0.1 → micro_reader-0.0.2}/README.md +17 -1
  3. {micro_reader-0.0.1 → micro_reader-0.0.2}/pyproject.toml +1 -1
  4. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/__init__.py +2 -0
  5. micro_reader-0.0.2/src/micro_reader/_io.py +159 -0
  6. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/czi.py +14 -0
  7. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/mrc.py +32 -1
  8. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/nd2.py +5 -1
  9. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/tiff.py +20 -0
  10. micro_reader-0.0.1/src/micro_reader/_io.py +0 -83
  11. {micro_reader-0.0.1 → micro_reader-0.0.2}/.gitignore +0 -0
  12. {micro_reader-0.0.1 → micro_reader-0.0.2}/LICENSE +0 -0
  13. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_axes.py +0 -0
  14. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_base.py +0 -0
  15. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_codecs.py +0 -0
  16. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_errors.py +0 -0
  17. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_index.py +0 -0
  18. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_jpeg_restarts.py +0 -0
  19. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_memory.py +0 -0
  20. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_metadata.py +0 -0
  21. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_pool.py +0 -0
  22. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_registry.py +0 -0
  23. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/_stream.py +0 -0
  24. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/__init__.py +0 -0
  25. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_czi_metadata.py +0 -0
  26. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_lif_metadata.py +0 -0
  27. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_nd2_legacy.py +0 -0
  28. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_nd2_metadata.py +0 -0
  29. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_nd2_structure.py +0 -0
  30. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/_tiff_metadata.py +0 -0
  31. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/ims.py +0 -0
  32. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/lif.py +0 -0
  33. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/oir.py +0 -0
  34. {micro_reader-0.0.1 → micro_reader-0.0.2}/src/micro_reader/backends/raster.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: micro-reader
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for.
5
5
  Project-URL: Homepage, https://github.com/bugraoezdemir/micro-reader
6
6
  Project-URL: Source, https://github.com/bugraoezdemir/micro-reader
@@ -677,7 +677,23 @@ You can also set it through the environment:
677
677
  export MICRO_READER_MEMORY_LIMIT=8GB
678
678
  ```
679
679
 
680
- For uncompressed data, only the requested rows and columns are read.
680
+ The limit covers the blocks being decoded at once. Decoded blocks are also kept for reuse,
681
+ so a block shared by neighbouring reads is not decoded twice. All open files share one
682
+ cache limit, 256 MB by default:
683
+
684
+ ```python
685
+ micro_reader.set_cache_limit("512MB")
686
+ micro_reader.cache_limit()
687
+ micro_reader.cached_bytes() # what the caches hold now
688
+ ```
689
+
690
+ or `export MICRO_READER_CACHE_LIMIT=512MB`. The process then uses at most the memory
691
+ limit plus the cache limit for decoding, however many files are open. One exception: a
692
+ single picture (PNG, JPEG, ... up to 256 MB) is kept decoded by its own open file.
693
+
694
+ For uncompressed data, only the requested rows are read. When you ask for part of
695
+ each row, the rest of the row is read along with it if that is faster than reading the
696
+ rows one by one.
681
697
 
682
698
  Compressed data is stored in blocks such as TIFF strips or tiles, CZI subblocks, and ND2 frames. Small blocks are decoded as a whole. Blocks larger than 16 MB are read in parts where the codec allows it.
683
699
 
@@ -625,7 +625,23 @@ You can also set it through the environment:
625
625
  export MICRO_READER_MEMORY_LIMIT=8GB
626
626
  ```
627
627
 
628
- For uncompressed data, only the requested rows and columns are read.
628
+ The limit covers the blocks being decoded at once. Decoded blocks are also kept for reuse,
629
+ so a block shared by neighbouring reads is not decoded twice. All open files share one
630
+ cache limit, 256 MB by default:
631
+
632
+ ```python
633
+ micro_reader.set_cache_limit("512MB")
634
+ micro_reader.cache_limit()
635
+ micro_reader.cached_bytes() # what the caches hold now
636
+ ```
637
+
638
+ or `export MICRO_READER_CACHE_LIMIT=512MB`. The process then uses at most the memory
639
+ limit plus the cache limit for decoding, however many files are open. One exception: a
640
+ single picture (PNG, JPEG, ... up to 256 MB) is kept decoded by its own open file.
641
+
642
+ For uncompressed data, only the requested rows are read. When you ask for part of
643
+ each row, the rest of the row is read along with it if that is faster than reading the
644
+ rows one by one.
629
645
 
630
646
  Compressed data is stored in blocks such as TIFF strips or tiles, CZI subblocks, and ND2 frames. Small blocks are decoded as a whole. Blocks larger than 16 MB are read in parts where the codec allows it.
631
647
 
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "micro-reader"
9
- version = "0.0.1"
9
+ version = "0.0.2"
10
10
  description = "Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.11"
@@ -8,6 +8,7 @@ from ._base import INDEX_NAMES, SAMPLED_INDICES, File, Image, ReadStats
8
8
  from ._metadata import Channel, Metadata, Quantity
9
9
  from ._errors import CorruptFileError, MicroReaderError, UnsupportedFormatError
10
10
  from ._memory import MemoryLimitError, memory_limit, set_memory_limit
11
+ from ._io import cache_limit, cached_bytes, set_cache_limit
11
12
  from ._pool import decode_threads, set_decode_threads
12
13
  from ._registry import BACKENDS, open # noqa: A004 - the public entry point
13
14
  from .backends.czi import DamagedFileWarning
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "open", "File", "Image", "INDEX_NAMES", "SAMPLED_INDICES", "ReadStats", "BACKENDS",
19
20
  "Metadata", "Channel", "Quantity",
20
21
  "decode_threads", "set_decode_threads", "memory_limit", "set_memory_limit",
22
+ "cache_limit", "set_cache_limit", "cached_bytes",
21
23
  "MicroReaderError", "UnsupportedFormatError", "CorruptFileError", "MemoryLimitError", "DamagedFileWarning",
22
24
  "__version__",
23
25
  ]
@@ -0,0 +1,159 @@
1
+ """Pieces every backend that reads segments on the decode pool needs."""
2
+ from __future__ import annotations
3
+
4
+ import builtins
5
+ import threading
6
+ from collections import OrderedDict
7
+
8
+ import numpy as np
9
+
10
+ from ._errors import CorruptFileError
11
+
12
+
13
+ class ThreadHandles:
14
+ """One read-only file handle per thread for one file (seek + read on a
15
+ shared handle is not thread-safe), kept open until :meth:`close`."""
16
+
17
+ def __init__(self, path: str):
18
+ self.path = path
19
+ self._local = threading.local()
20
+ self._lock = threading.Lock()
21
+ self._all: list = []
22
+
23
+ def get(self):
24
+ fh = getattr(self._local, "fh", None)
25
+ if fh is None:
26
+ fh = self._local.fh = builtins.open(self.path, "rb", buffering=0)
27
+ with self._lock:
28
+ self._all.append(fh)
29
+ return fh
30
+
31
+ def read(self, offset: int, size: int) -> bytes:
32
+ fh = self.get()
33
+ fh.seek(offset)
34
+ data = fh.read(size)
35
+ if len(data) != size:
36
+ raise CorruptFileError(f"{self.path!r}: data at byte {offset} ends early "
37
+ "(truncated file?)")
38
+ return data
39
+
40
+ def close(self) -> None:
41
+ with self._lock:
42
+ handles, self._all = self._all, []
43
+ for fh in handles:
44
+ fh.close()
45
+
46
+
47
+ #: what all decoded-block caches of a process may hold together, by default
48
+ DEFAULT_CACHE_LIMIT = 256 << 20
49
+
50
+ # One least-recently-used order over every cache of the process, so the number of
51
+ # open files does not multiply the memory the caches hold: (cache id, key) ->
52
+ # (cache, nbytes). Reentrant: a cache that is garbage-collected releases its bytes
53
+ # from __del__, which may run while this thread holds the lock.
54
+ _shared_lock = threading.RLock()
55
+ _shared: OrderedDict = OrderedDict()
56
+ _shared_bytes = 0
57
+ _cache_limit = None
58
+ _ids = iter(range(1 << 62))
59
+
60
+
61
+ def cache_limit() -> int:
62
+ """What all decoded-block caches of this process may hold together, in bytes."""
63
+ global _cache_limit
64
+ if _cache_limit is None:
65
+ import os
66
+ from ._memory import parse_size
67
+ env = os.environ.get("MICRO_READER_CACHE_LIMIT")
68
+ _cache_limit = parse_size(env) if env else DEFAULT_CACHE_LIMIT
69
+ return _cache_limit
70
+
71
+
72
+ def set_cache_limit(limit) -> None:
73
+ """Set what all decoded-block caches of this process may hold together (bytes or
74
+ e.g. "512MB"; 0 keeps nothing); None restores the default (the
75
+ ``MICRO_READER_CACHE_LIMIT`` environment variable, else 256 MB). Least recently
76
+ used blocks are dropped at once if the caches hold more."""
77
+ global _cache_limit
78
+ from ._memory import parse_size
79
+ with _shared_lock:
80
+ _cache_limit = None if limit is None else max(0, parse_size(limit))
81
+ _evict_shared(cache_limit())
82
+
83
+
84
+ def cached_bytes() -> int:
85
+ """What the decoded-block caches of this process hold now, in bytes."""
86
+ with _shared_lock:
87
+ return _shared_bytes
88
+
89
+
90
+ def _evict_shared(limit: int) -> None:
91
+ global _shared_bytes
92
+ while _shared_bytes > limit and _shared:
93
+ (_, key), (cache, nbytes) = _shared.popitem(last=False)
94
+ cache._drop(key)
95
+ _shared_bytes -= nbytes
96
+
97
+
98
+ class DecodeCache:
99
+ """Decoded segments (tiles, strips, chunks), least recently used out.
100
+
101
+ Keeps a segment shared by neighbouring reads from being decoded twice.
102
+ Segments above a quarter of ``max_bytes`` are not kept (a whole plane from
103
+ a single-strip file would evict everything else). ``max_bytes`` caps this
104
+ cache (one file or scene); every cache of the process together stays within
105
+ :func:`cache_limit`, so many open files do not hold many times the memory."""
106
+
107
+ def __init__(self, max_bytes: int):
108
+ self.max_bytes = max_bytes
109
+ self._items: OrderedDict = OrderedDict()
110
+ self._bytes = 0
111
+ self._id = next(_ids)
112
+
113
+ def get(self, key):
114
+ with _shared_lock:
115
+ seg = self._items.get(key)
116
+ if seg is not None:
117
+ self._items.move_to_end(key)
118
+ _shared.move_to_end((self._id, key))
119
+ return seg
120
+
121
+ def put(self, key, seg: np.ndarray) -> None:
122
+ global _shared_bytes
123
+ if seg.nbytes > self.max_bytes // 4 or seg.nbytes > cache_limit():
124
+ return
125
+ seg.setflags(write=False)
126
+ with _shared_lock:
127
+ if key in self._items:
128
+ return
129
+ self._items[key] = seg
130
+ self._bytes += seg.nbytes
131
+ _shared[(self._id, key)] = (self, seg.nbytes)
132
+ _shared_bytes += seg.nbytes
133
+ while self._bytes > self.max_bytes:
134
+ old_key = next(iter(self._items))
135
+ _shared.pop((self._id, old_key))
136
+ _shared_bytes -= self._items[old_key].nbytes
137
+ self._drop(old_key)
138
+ _evict_shared(cache_limit())
139
+
140
+ def _drop(self, key) -> None:
141
+ """Forget *key* (the shared order is updated by the caller)."""
142
+ seg = self._items.pop(key, None)
143
+ if seg is not None:
144
+ self._bytes -= seg.nbytes
145
+
146
+ def clear(self) -> None:
147
+ global _shared_bytes
148
+ with _shared_lock:
149
+ for key, seg in self._items.items():
150
+ if _shared.pop((self._id, key), None) is not None:
151
+ _shared_bytes -= seg.nbytes
152
+ self._items.clear()
153
+ self._bytes = 0
154
+
155
+ def __del__(self):
156
+ try:
157
+ self.clear()
158
+ except Exception: # noqa: BLE001 - interpreter exit
159
+ pass
@@ -548,6 +548,20 @@ class _CziImage(Image):
548
548
  fh.seek(base + (y0 - sy0) * s.width * item)
549
549
  if fh.readinto(memoryview(buf).cast("B")) != buf.nbytes:
550
550
  raise CorruptFileError(f"{self._czi.path!r}: subblock data ends early")
551
+ elif y1 - y0 > 1 and (s.width - width) * item <= SEPARATE_READ_BYTES:
552
+ # a few skipped bytes per row cost less than one more read per row: the span
553
+ # from the first row's columns to the last row's, in one read
554
+ rows, row = y1 - y0, s.width * self._samples
555
+ span = (rows - 1) * row + width * self._samples
556
+ flat = np.empty(span, self._file_dtype)
557
+ fh.seek(base + ((y0 - sy0) * s.width + (x0 - sx0)) * item)
558
+ if fh.readinto(memoryview(flat).cast("B")) != flat.nbytes:
559
+ raise CorruptFileError(f"{self._czi.path!r}: subblock data ends early")
560
+ buf = np.lib.stride_tricks.as_strided(
561
+ flat, shape=(rows, width * self._samples),
562
+ strides=(row * flat.itemsize, flat.itemsize))
563
+ self._place(buf, target, rows, width)
564
+ return span
551
565
  else:
552
566
  buf = np.empty((y1 - y0, width * self._samples), self._file_dtype)
553
567
  for i in range(y1 - y0):
@@ -31,6 +31,7 @@ from .._metadata import Channel, Metadata, quantity
31
31
  NAME = "mrc"
32
32
  HEADER_BYTES = 1024
33
33
  CHUNK_BYTES = 4 << 20 # full-width rows are read this many bytes at a time
34
+ SEPARATE_READ_BYTES = 32 << 10 # bytes worth skipping rather than issuing one more read
34
35
  EXTENSIONS = {".mrc", ".mrcs", ".map", ".st", ".ali", ".rec"}
35
36
 
36
37
  #: MRC2014 mode -> numpy dtype. Mode 0 is *signed* int8 in MRC2014; older
@@ -93,9 +94,13 @@ class _MrcImage(Image):
93
94
  # bottom of the image. Rows are returned top-down, as Bio-Formats
94
95
  # (and so eubi-bridge, Fiji, OME-Zarr viewers) presents them: display
95
96
  # row y is file row ny-1-y.
97
+ read = out.size
96
98
  with builtins.open(self._path, "rb", buffering=0) as fh:
97
99
  if xs.stop - xs.start == nx:
98
100
  self._read_full_rows(fh, zs, ys, out)
101
+ elif (ys.stop - ys.start > 1
102
+ and (nx - (xs.stop - xs.start)) * item <= SEPARATE_READ_BYTES):
103
+ read = self._read_row_spans(fh, zs, ys, xs, out)
99
104
  else:
100
105
  length = out.shape[2] * item
101
106
  for i, z in enumerate(range(zs.start, zs.stop)):
@@ -107,7 +112,33 @@ class _MrcImage(Image):
107
112
  f"{self._path!r}: short read at section {z}")
108
113
  if not self._file_dtype.isnative:
109
114
  out = out.byteswap(inplace=True).view(self.dtype)
110
- return out, out.size
115
+ return out, read
116
+
117
+ def _read_row_spans(self, fh, zs, ys, xs, out) -> int:
118
+ """Part of each row, when the columns skipped per row are few: they cost less
119
+ than one more read per row (as in LIF). Each chunk of rows is one read, from the
120
+ first row's columns to the last row's, reversed into place (bottom-up rows).
121
+ Returns the values read."""
122
+ nz, ny, nx = self.shape
123
+ item = self._file_dtype.itemsize
124
+ width = xs.stop - xs.start
125
+ per_chunk = max(1, CHUNK_BYTES // (nx * item))
126
+ flat = np.empty(min(per_chunk, ys.stop - ys.start) * nx, dtype=self._file_dtype)
127
+ first, stop = ny - ys.stop, ny - ys.start # file rows, bottom-up
128
+ read = 0
129
+ for i, z in enumerate(range(zs.start, zs.stop)):
130
+ for f in range(first, stop, per_chunk):
131
+ n = min(per_chunk, stop - f)
132
+ span = (n - 1) * nx + width
133
+ fh.seek(self._offset + ((z * ny + f) * nx + xs.start) * item)
134
+ if fh.readinto(memoryview(flat[:span]).cast("B")) != span * item:
135
+ raise CorruptFileError(f"{self._path!r}: short read at section {z}")
136
+ rows = np.lib.stride_tricks.as_strided(flat, shape=(n, width),
137
+ strides=(nx * item, item))
138
+ top = ny - f - n - ys.start # display row of file row f+n-1
139
+ out[i, top:top + n] = rows[::-1]
140
+ read += span
141
+ return read
111
142
 
112
143
  def _read_full_rows(self, fh, zs, ys, out):
113
144
  """Full-width rows of a section are contiguous in the file: read them
@@ -36,6 +36,7 @@ from . import _nd2_structure as structure
36
36
  NAME = "nd2"
37
37
  DECODE_CACHE_BYTES = 64 << 20
38
38
  RAW_TASK_BYTES = 8 << 20 # uncompressed frames are read in row pieces of this size
39
+ SEPARATE_READ_BYTES = 32 << 10 # bytes worth skipping rather than issuing one more read
39
40
  STREAM_PIECE_BYTES = 8 << 20 # rows of a streamed "lossless" frame decoded at a time
40
41
  TIMESTAMP_BYTES = 8 # each frame chunk starts with a float64 timestamp
41
42
 
@@ -168,7 +169,10 @@ class _Nd2Image(Image):
168
169
  def _raw_tasks(self, start, dst, cs, ys, xs, ss):
169
170
  f, a = self._f, self._f.attrs
170
171
  per_task = max(1, RAW_TASK_BYTES // a.width_bytes)
171
- whole_rows = (xs.stop - xs.start) * 2 >= a.width # else: only the needed bytes per row
172
+ # whole rows when the columns skipped per row are few: fewer bytes than another read
173
+ # call costs (as in LIF), or at most half the row; else only the needed bytes per row
174
+ skipped = a.width_bytes - (xs.stop - xs.start) * f.pixel_bytes
175
+ whole_rows = (xs.stop - xs.start) * 2 >= a.width or skipped <= SEPARATE_READ_BYTES
172
176
  for y0 in range(ys.start, ys.stop, per_task):
173
177
  y1 = min(ys.stop, y0 + per_task)
174
178
  part = dst[:, y0 - ys.start:y1 - ys.start]
@@ -54,6 +54,7 @@ NAME = "tiff"
54
54
  MAGIC = (b"II*\x00", b"MM\x00*", b"II+\x00", b"MM\x00+") # classic and BigTIFF
55
55
  CHUNK_BYTES = 4 << 20 # buffered raw rows are read this many bytes at a time
56
56
  RAW_TASK_BYTES = 8 << 20 # uncompressed strip runs are split into pool tasks of this size
57
+ SEPARATE_READ_BYTES = 32 << 10 # bytes worth skipping rather than issuing one more read
57
58
  DECODE_CACHE_BYTES = 64 << 20 # decoded segments kept per scene
58
59
  STREAM_PIECE_BYTES = 8 << 20 # rows of a streamed segment decoded at a time (reserved)
59
60
  #: image codecs that decode a rectangle alone (``_area_into``)
@@ -467,6 +468,25 @@ class _TiffImage(Image):
467
468
  fh.seek(offset + rows[0] * row_len * item)
468
469
  _readinto(fh, dst, self._path)
469
470
  return dst.size
471
+ width = cols[1] - cols[0]
472
+ if (cols != (0, stride) and rows[1] - rows[0] > 1
473
+ and (stride - width) * nc * item <= SEPARATE_READ_BYTES):
474
+ # a few skipped bytes per row cost less than one more read per row (as in
475
+ # LIF): read the span from the first row's columns to the last row's, in
476
+ # chunks, and keep the columns asked for
477
+ per_chunk = max(1, CHUNK_BYTES // (row_len * item))
478
+ flat = np.empty(min(per_chunk, rows[1] - rows[0]) * row_len, self._file_dtype)
479
+ read = 0
480
+ for a in range(rows[0], rows[1], per_chunk):
481
+ n = min(per_chunk, rows[1] - a)
482
+ span = (n - 1) * row_len + width * nc
483
+ fh.seek(offset + (a * stride + cols[0]) * nc * item)
484
+ _readinto(fh, flat[:span], self._path)
485
+ rows_view = np.lib.stride_tricks.as_strided(
486
+ flat, shape=(n, width, nc), strides=(row_len * item, nc * item, item))
487
+ dst[a - rows[0]:a - rows[0] + n] = rows_view[..., cs]
488
+ read += span
489
+ return read
470
490
  if cols != (0, stride) and direct and dst[0].flags.c_contiguous:
471
491
  width = cols[1] - cols[0]
472
492
  for r in range(rows[0], rows[1]):
@@ -1,83 +0,0 @@
1
- """Pieces every backend that reads segments on the decode pool needs."""
2
- from __future__ import annotations
3
-
4
- import builtins
5
- import threading
6
- from collections import OrderedDict
7
-
8
- import numpy as np
9
-
10
- from ._errors import CorruptFileError
11
-
12
-
13
- class ThreadHandles:
14
- """One read-only file handle per thread for one file (seek + read on a
15
- shared handle is not thread-safe), kept open until :meth:`close`."""
16
-
17
- def __init__(self, path: str):
18
- self.path = path
19
- self._local = threading.local()
20
- self._lock = threading.Lock()
21
- self._all: list = []
22
-
23
- def get(self):
24
- fh = getattr(self._local, "fh", None)
25
- if fh is None:
26
- fh = self._local.fh = builtins.open(self.path, "rb", buffering=0)
27
- with self._lock:
28
- self._all.append(fh)
29
- return fh
30
-
31
- def read(self, offset: int, size: int) -> bytes:
32
- fh = self.get()
33
- fh.seek(offset)
34
- data = fh.read(size)
35
- if len(data) != size:
36
- raise CorruptFileError(f"{self.path!r}: data at byte {offset} ends early "
37
- "(truncated file?)")
38
- return data
39
-
40
- def close(self) -> None:
41
- with self._lock:
42
- handles, self._all = self._all, []
43
- for fh in handles:
44
- fh.close()
45
-
46
-
47
- class DecodeCache:
48
- """Decoded segments (tiles, strips, chunks), least recently used out.
49
-
50
- Keeps a segment shared by neighbouring reads from being decoded twice.
51
- Segments above a quarter of the budget are not kept (a whole plane from
52
- a single-strip file would evict everything else)."""
53
-
54
- def __init__(self, max_bytes: int):
55
- self.max_bytes = max_bytes
56
- self._items: OrderedDict = OrderedDict()
57
- self._bytes = 0
58
- self._lock = threading.Lock()
59
-
60
- def get(self, key):
61
- with self._lock:
62
- seg = self._items.get(key)
63
- if seg is not None:
64
- self._items.move_to_end(key)
65
- return seg
66
-
67
- def put(self, key, seg: np.ndarray) -> None:
68
- if seg.nbytes > self.max_bytes // 4:
69
- return
70
- seg.setflags(write=False)
71
- with self._lock:
72
- if key in self._items:
73
- return
74
- self._items[key] = seg
75
- self._bytes += seg.nbytes
76
- while self._bytes > self.max_bytes:
77
- _, old = self._items.popitem(last=False)
78
- self._bytes -= old.nbytes
79
-
80
- def clear(self) -> None:
81
- with self._lock:
82
- self._items.clear()
83
- self._bytes = 0
File without changes
File without changes