Coverage for python/lsst/resources/_resourceHandles/_davResourceHandle.py: 25%
101 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:18 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:18 -0700
1# This file is part of lsst-resources.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("DavReadResourceHandle",)
16import io
17import logging
18from collections.abc import Callable, Iterable
19from typing import TYPE_CHECKING, AnyStr
21from ..davutils import DavFileMetadata
22from ._baseResourceHandle import BaseResourceHandle, CloseStatus
24if TYPE_CHECKING:
25 from ..dav import DavResourcePath
28class DavReadResourceHandle(BaseResourceHandle[bytes]):
29 """WebDAV-based specialization of `.BaseResourceHandle`.
31 Parameters
32 ----------
33 mode : `str`
34 Handle modes as described in the python `io` module.
35 log : `~logging.Logger`
36 Logger to used when writing messages.
37 uri : `lsst.resources.dav.DavResourcePath`
38 URI of remote resource.
39 newline : `str` or `None`, optional
40 When doing multiline operations, break the stream on given character.
41 Defaults to newline. If a file is opened in binary mode, this argument
42 is not used, as binary files will only split lines on the binary
43 newline representation.
44 """
46 def __init__(
47 self,
48 mode: str,
49 log: logging.Logger,
50 uri: DavResourcePath,
51 stat: DavFileMetadata,
52 *,
53 newline: AnyStr | None = None,
54 ) -> None:
55 super().__init__(mode, log, uri, newline=newline)
56 self._uri: DavResourcePath = uri
57 self._stat: DavFileMetadata = stat
58 self._current_position = 0
59 self._cache: io.BytesIO | None = None
60 self._buffer: io.BytesIO | None = None
61 self._closed = CloseStatus.OPEN
63 def close(self) -> None:
64 self._closed = CloseStatus.CLOSED
65 self._cache = None
67 @property
68 def closed(self) -> bool:
69 return self._closed == CloseStatus.CLOSED
71 def fileno(self) -> int:
72 raise io.UnsupportedOperation("DavReadResourceHandle does not have a file number")
74 def flush(self) -> None:
75 modes = set(self._mode)
76 if {"w", "x", "a", "+"} & modes:
77 raise io.UnsupportedOperation("DavReadResourceHandles are read only")
79 @property
80 def isatty(self) -> bool | Callable[[], bool]:
81 return False
83 def readable(self) -> bool:
84 return True
86 def readline(self, size: int = -1) -> bytes:
87 raise io.UnsupportedOperation("DavReadResourceHandles Do not support line by line reading")
89 def readlines(self, size: int = -1) -> Iterable[bytes]:
90 raise io.UnsupportedOperation("DavReadResourceHandles Do not support line by line reading")
92 def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
93 match whence:
94 case io.SEEK_SET:
95 if offset < 0:
96 raise ValueError(f"negative seek value {offset}")
97 self._current_position = offset
98 case io.SEEK_CUR:
99 self._current_position += offset
100 case io.SEEK_END:
101 self._current_position = self._stat.size + offset
102 case _:
103 raise ValueError(f"unexpected value {whence} for whence in seek()")
105 if self._current_position < 0:
106 self._current_position = 0
108 return self._current_position
110 def seekable(self) -> bool:
111 return True
113 def tell(self) -> int:
114 return self._current_position
116 def truncate(self, size: int | None = None) -> int:
117 raise io.UnsupportedOperation("DavReadResourceHandles Do not support truncation")
119 def writable(self) -> bool:
120 return False
122 def write(self, b: bytes, /) -> int:
123 raise io.UnsupportedOperation("DavReadResourceHandles are read only")
125 def writelines(self, b: Iterable[bytes], /) -> None:
126 raise io.UnsupportedOperation("DavReadResourceHandles are read only")
128 @property
129 def _eof(self) -> bool:
130 return self._current_position >= self._stat.size
132 def _download_to_cache(self) -> io.BytesIO:
133 """Download the entire content of the remote resource to an internal
134 memory buffer.
135 """
136 if self._cache is None:
137 self._cache = io.BytesIO()
138 self._cache.write(self._uri.read())
140 return self._cache
142 def read(self, size: int = -1) -> bytes:
143 if self._eof or size == 0:
144 return b""
146 # If this file's size is small than the buffer size configured for
147 # this URI's client, download the entire file in one request and cache
148 # its content. This avoids multiple roundtrips to the server
149 # for retrieving small chunks.
150 if self._stat.size <= self._uri._client._config.buffer_size:
151 self._download_to_cache()
153 # If we are asked to read the whole file content, cache the entire
154 # file content and return a copy-on-write memory view of our internal
155 # cache.
156 if self._current_position == 0 and size == -1:
157 cache = self._download_to_cache()
158 self._current_position = self._stat.size
159 return cache.getvalue()
161 # This is a partial read. If we have already cached the whole file
162 # content use the cache to build the return value.
163 if self._cache is not None:
164 start = self._current_position
165 end = self._current_position = self._stat.size if size < 0 else start + size
166 return self._cache.getvalue()[start:end]
168 # We need to make a partial read from the server. Reuse our internal
169 # I/O buffer to reduce memory allocations.
170 if self._buffer is None:
171 self._buffer = io.BytesIO()
173 start = self._current_position
174 end = self._stat.size if size < 0 else min(start + size, self._stat.size)
175 self._buffer.seek(0)
176 self._buffer.write(self._uri.read_range(start=start, end=end - 1))
177 count = self._buffer.tell()
178 self._current_position += count
179 return self._buffer.getvalue()[0:count]
181 def readinto(self, output: bytearray) -> int:
182 """Read up to `len(output)` bytes into `output` and return the number
183 of bytes read.
184 """
185 if self._eof or len(output) == 0:
186 return 0
188 data = self.read(len(output))
189 output[:] = data
190 return len(data)