lsst-resources 29.2025.1700__py3-none-any.whl → 29.2025.4600__py3-none-any.whl

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.
@@ -0,0 +1,197 @@
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.
11
+
12
+ from __future__ import annotations
13
+
14
+ __all__ = ("DavReadResourceHandle",)
15
+
16
+ import io
17
+ import logging
18
+ from collections.abc import Callable, Iterable
19
+ from typing import TYPE_CHECKING, AnyStr
20
+
21
+ from ..davutils import DavFileMetadata
22
+ from ._baseResourceHandle import BaseResourceHandle, CloseStatus
23
+
24
+ if TYPE_CHECKING:
25
+ from ..dav import DavResourcePath
26
+
27
+
28
+ class DavReadResourceHandle(BaseResourceHandle[bytes]):
29
+ """WebDAV-based specialization of `.BaseResourceHandle`.
30
+
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
+ stat : `DavFileMetadata`
40
+ Information about this resource.
41
+ newline : `str` or `None`, optional
42
+ When doing multiline operations, break the stream on given character.
43
+ Defaults to newline. If a file is opened in binary mode, this argument
44
+ is not used, as binary files will only split lines on the binary
45
+ newline representation.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ mode: str,
51
+ log: logging.Logger,
52
+ uri: DavResourcePath,
53
+ stat: DavFileMetadata,
54
+ *,
55
+ newline: AnyStr | None = None,
56
+ ) -> None:
57
+ super().__init__(mode, log, uri, newline=newline)
58
+ self._uri: DavResourcePath = uri
59
+ self._stat: DavFileMetadata = stat
60
+ self._current_position = 0
61
+ self._cache: io.BytesIO | None = None
62
+ self._buffer: io.BytesIO | None = None
63
+ self._closed = CloseStatus.OPEN
64
+
65
+ def close(self) -> None:
66
+ self._closed = CloseStatus.CLOSED
67
+ self._cache = None
68
+
69
+ @property
70
+ def closed(self) -> bool:
71
+ return self._closed == CloseStatus.CLOSED
72
+
73
+ def fileno(self) -> int:
74
+ raise io.UnsupportedOperation("DavReadResourceHandle does not have a file number")
75
+
76
+ def flush(self) -> None:
77
+ modes = set(self._mode)
78
+ if {"w", "x", "a", "+"} & modes:
79
+ raise io.UnsupportedOperation("DavReadResourceHandles are read only")
80
+
81
+ @property
82
+ def isatty(self) -> bool | Callable[[], bool]:
83
+ return False
84
+
85
+ def readable(self) -> bool:
86
+ return True
87
+
88
+ def readline(self, size: int = -1) -> bytes:
89
+ raise io.UnsupportedOperation("DavReadResourceHandles Do not support line by line reading")
90
+
91
+ def readlines(self, size: int = -1) -> Iterable[bytes]:
92
+ raise io.UnsupportedOperation("DavReadResourceHandles Do not support line by line reading")
93
+
94
+ def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
95
+ match whence:
96
+ case io.SEEK_SET:
97
+ if offset < 0:
98
+ raise ValueError(f"negative seek value {offset}")
99
+ self._current_position = offset
100
+ case io.SEEK_CUR:
101
+ self._current_position += offset
102
+ case io.SEEK_END:
103
+ self._current_position = self._stat.size + offset
104
+ case _:
105
+ raise ValueError(f"unexpected value {whence} for whence in seek()")
106
+
107
+ if self._current_position < 0:
108
+ self._current_position = 0
109
+
110
+ return self._current_position
111
+
112
+ def seekable(self) -> bool:
113
+ return True
114
+
115
+ def tell(self) -> int:
116
+ return self._current_position
117
+
118
+ def truncate(self, size: int | None = None) -> int:
119
+ raise io.UnsupportedOperation("DavReadResourceHandles Do not support truncation")
120
+
121
+ def writable(self) -> bool:
122
+ return False
123
+
124
+ def write(self, b: bytes, /) -> int:
125
+ raise io.UnsupportedOperation("DavReadResourceHandles are read only")
126
+
127
+ def writelines(self, b: Iterable[bytes], /) -> None:
128
+ raise io.UnsupportedOperation("DavReadResourceHandles are read only")
129
+
130
+ @property
131
+ def _eof(self) -> bool:
132
+ return self._current_position >= self._stat.size
133
+
134
+ def _download_to_cache(self) -> io.BytesIO:
135
+ """Download the entire content of the remote resource to an internal
136
+ memory buffer.
137
+ """
138
+ if self._cache is None:
139
+ self._cache = io.BytesIO()
140
+ self._cache.write(self._uri.read())
141
+
142
+ return self._cache
143
+
144
+ def read(self, size: int = -1) -> bytes:
145
+ if self._eof or size == 0:
146
+ return b""
147
+
148
+ # If this file's size is small than the buffer size configured for
149
+ # this URI's client, download the entire file in one request and cache
150
+ # its content. This avoids multiple roundtrips to the server
151
+ # for retrieving small chunks.
152
+ if self._stat.size <= self._uri._client._config.buffer_size:
153
+ self._download_to_cache()
154
+
155
+ # If we are asked to read the whole file content, cache the entire
156
+ # file content and return a copy-on-write memory view of our internal
157
+ # cache.
158
+ if self._current_position == 0 and size == -1:
159
+ cache = self._download_to_cache()
160
+ self._current_position = self._stat.size
161
+ return cache.getvalue()
162
+
163
+ # This is a partial read. If we have already cached the whole file
164
+ # content use the cache to build the return value.
165
+ if self._cache is not None:
166
+ start = self._current_position
167
+ end = self._current_position = self._stat.size if size < 0 else start + size
168
+ return self._cache.getvalue()[start:end]
169
+
170
+ # We need to make a partial read from the server. Reuse our internal
171
+ # I/O buffer to reduce memory allocations.
172
+ if self._buffer is None:
173
+ self._buffer = io.BytesIO()
174
+
175
+ start = self._current_position
176
+ end = self._stat.size if size < 0 else min(start + size, self._stat.size)
177
+ self._buffer.seek(0)
178
+ self._buffer.write(self._uri.read_range(start=start, end=end - 1))
179
+ count = self._buffer.tell()
180
+ self._current_position += count
181
+ return self._buffer.getvalue()[0:count]
182
+
183
+ def readinto(self, output: bytearray) -> int:
184
+ """Read up to `len(output)` bytes into `output` and return the number
185
+ of bytes read.
186
+
187
+ Parameters
188
+ ----------
189
+ output : `bytearray`
190
+ Byte array to write output into.
191
+ """
192
+ if self._eof or len(output) == 0:
193
+ return 0
194
+
195
+ data = self.read(len(output))
196
+ output[:] = data
197
+ return len(data)
@@ -79,7 +79,7 @@ class FileResourceHandle(BaseResourceHandle[U]):
79
79
  return self._fileHandle.fileno()
80
80
 
81
81
  def flush(self) -> None:
82
- self._fileHandle.close()
82
+ self._fileHandle.flush()
83
83
 
84
84
  @property
85
85
  def isatty(self) -> bool:
@@ -168,7 +168,9 @@ class HttpReadResourceHandle(BaseResourceHandle[bytes]):
168
168
  # return the result
169
169
  self._completeBuffer = io.BytesIO()
170
170
  with time_this(self._log, msg="Read from remote resource %s", args=(self._url,)):
171
- resp = self._session.get(_dav_to_http(self._url), stream=False, timeout=self._timeout)
171
+ with self._session as session:
172
+ resp = session.get(_dav_to_http(self._url), stream=False, timeout=self._timeout)
173
+
172
174
  if (code := resp.status_code) not in (requests.codes.ok, requests.codes.partial):
173
175
  raise FileNotFoundError(f"Unable to read resource {self._url}; status code: {code}")
174
176
  self._completeBuffer.write(resp.content)
@@ -190,9 +192,10 @@ class HttpReadResourceHandle(BaseResourceHandle[bytes]):
190
192
  with time_this(
191
193
  self._log, msg="Read from remote resource %s using headers %s", args=(self._url, headers)
192
194
  ):
193
- resp = self._session.get(
194
- _dav_to_http(self._url), stream=False, timeout=self._timeout, headers=headers
195
- )
195
+ with self._session as session:
196
+ resp = session.get(
197
+ _dav_to_http(self._url), stream=False, timeout=self._timeout, headers=headers
198
+ )
196
199
 
197
200
  if resp.status_code == requests.codes.range_not_satisfiable:
198
201
  # Must have run off the end of the file. A standard file handle
@@ -14,14 +14,12 @@ from __future__ import annotations
14
14
  __all__ = ("S3ResourceHandle",)
15
15
 
16
16
  import logging
17
- import warnings
18
17
  from collections.abc import Iterable, Mapping
19
18
  from io import SEEK_CUR, SEEK_END, SEEK_SET, BytesIO, UnsupportedOperation
20
19
  from typing import TYPE_CHECKING
21
20
 
22
21
  from botocore.exceptions import ClientError
23
22
 
24
- from lsst.utils.introspection import find_outside_stacklevel
25
23
  from lsst.utils.timer import time_this
26
24
 
27
25
  from ..s3utils import all_retryable_errors, backoff, max_retry_time, translate_client_error
@@ -168,21 +166,9 @@ class S3ResourceHandle(BaseResourceHandle[bytes]):
168
166
  # written to.
169
167
  s3_min_bits = 5 * 1024 * 1024 # S3 flush threshold is 5 Mib.
170
168
  if (
171
- (self.tell() - (self._last_flush_position or 0)) < s3_min_bits
172
- and self._closed != CloseStatus.CLOSING
173
- and not self._warned
174
- ):
175
- amount = s3_min_bits / (1024 * 1024)
176
- warnings.warn(
177
- f"S3 does not support flushing objects less than {amount} Mib, skipping",
178
- stacklevel=find_outside_stacklevel(
179
- "lsst.resources",
180
- "backoff",
181
- "contextlib",
182
- allow_modules={"lsst.resources.tests"},
183
- ),
184
- )
185
- self._warned = True
169
+ self.tell() - (self._last_flush_position or 0)
170
+ ) < s3_min_bits and self._closed != CloseStatus.CLOSING:
171
+ # Return until the buffer is big enough.
186
172
  return
187
173
  # nothing to write, don't create an empty upload
188
174
  if self.tell() == 0: