lsst-resources 29.0.0rc7__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.
- lsst/resources/_resourceHandles/_davResourceHandle.py +197 -0
- lsst/resources/_resourceHandles/_fileResourceHandle.py +1 -1
- lsst/resources/_resourceHandles/_httpResourceHandle.py +16 -2
- lsst/resources/_resourceHandles/_s3ResourceHandle.py +3 -17
- lsst/resources/_resourcePath.py +448 -81
- lsst/resources/dav.py +912 -0
- lsst/resources/davutils.py +2659 -0
- lsst/resources/file.py +97 -57
- lsst/resources/gs.py +11 -4
- lsst/resources/http.py +229 -62
- lsst/resources/mem.py +7 -1
- lsst/resources/packageresource.py +13 -2
- lsst/resources/s3.py +174 -17
- lsst/resources/s3utils.py +8 -1
- lsst/resources/schemeless.py +6 -3
- lsst/resources/tests.py +140 -12
- lsst/resources/utils.py +74 -1
- lsst/resources/version.py +1 -1
- {lsst_resources-29.0.0rc7.dist-info → lsst_resources-29.2025.4600.dist-info}/METADATA +3 -3
- lsst_resources-29.2025.4600.dist-info/RECORD +31 -0
- {lsst_resources-29.0.0rc7.dist-info → lsst_resources-29.2025.4600.dist-info}/WHEEL +1 -1
- lsst_resources-29.0.0rc7.dist-info/RECORD +0 -28
- {lsst_resources-29.0.0rc7.dist-info → lsst_resources-29.2025.4600.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_resources-29.0.0rc7.dist-info → lsst_resources-29.2025.4600.dist-info}/licenses/LICENSE +0 -0
- {lsst_resources-29.0.0rc7.dist-info → lsst_resources-29.2025.4600.dist-info}/top_level.txt +0 -0
- {lsst_resources-29.0.0rc7.dist-info → lsst_resources-29.2025.4600.dist-info}/zip-safe +0 -0
lsst/resources/file.py
CHANGED
|
@@ -21,6 +21,7 @@ import os.path
|
|
|
21
21
|
import posixpath
|
|
22
22
|
import re
|
|
23
23
|
import shutil
|
|
24
|
+
import stat
|
|
24
25
|
import urllib.parse
|
|
25
26
|
from collections.abc import Iterator
|
|
26
27
|
from typing import IO, TYPE_CHECKING
|
|
@@ -79,19 +80,27 @@ class FileResourcePath(ResourcePath):
|
|
|
79
80
|
"""Remove the resource."""
|
|
80
81
|
os.remove(self.ospath)
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
@contextlib.contextmanager
|
|
84
|
+
def _as_local(
|
|
85
|
+
self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
|
|
86
|
+
) -> Iterator[ResourcePath]:
|
|
83
87
|
"""Return the local path of the file.
|
|
84
88
|
|
|
85
89
|
This is an internal helper for ``as_local()``.
|
|
86
90
|
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
multithreaded : `bool`, optional
|
|
94
|
+
Unused.
|
|
95
|
+
tmpdir : `ResourcePath` or `None`, optional
|
|
96
|
+
Unused.
|
|
97
|
+
|
|
87
98
|
Returns
|
|
88
99
|
-------
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
temporary : `bool`
|
|
92
|
-
Always returns the temporary nature of the input file resource.
|
|
100
|
+
local_uri : `ResourcePath`
|
|
101
|
+
A local URI. In this case it will be itself.
|
|
93
102
|
"""
|
|
94
|
-
|
|
103
|
+
yield self
|
|
95
104
|
|
|
96
105
|
def read(self, size: int = -1) -> bytes:
|
|
97
106
|
with open(self.ospath, "rb") as fh:
|
|
@@ -99,7 +108,7 @@ class FileResourcePath(ResourcePath):
|
|
|
99
108
|
|
|
100
109
|
def write(self, data: bytes, overwrite: bool = True) -> None:
|
|
101
110
|
dir = os.path.dirname(self.ospath)
|
|
102
|
-
if not os.path.exists(dir):
|
|
111
|
+
if dir and not os.path.exists(dir):
|
|
103
112
|
_create_directories(dir)
|
|
104
113
|
mode = "wb" if overwrite else "xb"
|
|
105
114
|
with open(self.ospath, mode) as f:
|
|
@@ -141,6 +150,7 @@ class FileResourcePath(ResourcePath):
|
|
|
141
150
|
transfer: str,
|
|
142
151
|
overwrite: bool = False,
|
|
143
152
|
transaction: TransactionProtocol | None = None,
|
|
153
|
+
multithreaded: bool = True,
|
|
144
154
|
) -> None:
|
|
145
155
|
"""Transfer the current resource to a local file.
|
|
146
156
|
|
|
@@ -155,6 +165,8 @@ class FileResourcePath(ResourcePath):
|
|
|
155
165
|
Allow an existing file to be overwritten. Defaults to `False`.
|
|
156
166
|
transaction : `~lsst.resources.utils.TransactionProtocol`, optional
|
|
157
167
|
If a transaction is provided, undo actions will be registered.
|
|
168
|
+
multithreaded : `bool`, optional
|
|
169
|
+
Whether threads are allowed to be used or not.
|
|
158
170
|
"""
|
|
159
171
|
# Fail early to prevent delays if remote resources are requested
|
|
160
172
|
if transfer not in self.transferModes:
|
|
@@ -172,9 +184,53 @@ class FileResourcePath(ResourcePath):
|
|
|
172
184
|
transfer,
|
|
173
185
|
)
|
|
174
186
|
|
|
187
|
+
# The output location should not exist unless overwrite=True.
|
|
188
|
+
# Rather than use `exists()`, use os.stat since we might need
|
|
189
|
+
# the full answer later.
|
|
190
|
+
dest_stat: os.stat_result | None
|
|
191
|
+
try:
|
|
192
|
+
# Do not read through links of the file itself.
|
|
193
|
+
dest_stat = os.lstat(self.ospath)
|
|
194
|
+
except FileNotFoundError:
|
|
195
|
+
dest_stat = None
|
|
196
|
+
|
|
197
|
+
# It is possible that the source URI and target URI refer
|
|
198
|
+
# to the same file. This can happen for a number of reasons
|
|
199
|
+
# (such as soft links in the path, or they really are the same).
|
|
200
|
+
# In that case log a message and return as if the transfer
|
|
201
|
+
# completed (it technically did). A temporary file download
|
|
202
|
+
# can't be the same so the test can be skipped.
|
|
203
|
+
if dest_stat and src.isLocal and not src.isTemporary:
|
|
204
|
+
# Be consistent and use lstat here (even though realpath
|
|
205
|
+
# has been called). It does not harm.
|
|
206
|
+
local_src_stat = os.lstat(src.ospath)
|
|
207
|
+
if dest_stat.st_ino == local_src_stat.st_ino and dest_stat.st_dev == local_src_stat.st_dev:
|
|
208
|
+
log.debug(
|
|
209
|
+
"Destination URI %s is the same file as source URI %s, returning immediately."
|
|
210
|
+
" No further action required.",
|
|
211
|
+
self,
|
|
212
|
+
src,
|
|
213
|
+
)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if not overwrite and dest_stat:
|
|
217
|
+
raise FileExistsError(
|
|
218
|
+
f"Destination path '{self}' already exists. Transfer from {src} cannot be completed."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Make the destination path absolute (but don't follow links since
|
|
222
|
+
# that would possibly cause us to end up in the wrong place if the
|
|
223
|
+
# file existed already as a soft link)
|
|
224
|
+
newFullPath = os.path.abspath(self.ospath)
|
|
225
|
+
outputDir = os.path.dirname(newFullPath)
|
|
226
|
+
|
|
175
227
|
# We do not have to special case FileResourcePath here because
|
|
176
|
-
# as_local handles that.
|
|
177
|
-
|
|
228
|
+
# as_local handles that. If remote download, download it to the
|
|
229
|
+
# destination directory to allow an atomic rename but only if that
|
|
230
|
+
# directory exists because we do not want to create a directory
|
|
231
|
+
# but then end up with the download failing.
|
|
232
|
+
tmpdir = outputDir if os.path.exists(outputDir) else None
|
|
233
|
+
with src.as_local(multithreaded=multithreaded, tmpdir=tmpdir) as local_uri:
|
|
178
234
|
is_temporary = local_uri.isTemporary
|
|
179
235
|
local_src = local_uri.ospath
|
|
180
236
|
|
|
@@ -228,45 +284,6 @@ class FileResourcePath(ResourcePath):
|
|
|
228
284
|
if src != local_uri and is_temporary and transfer == "copy":
|
|
229
285
|
transfer = "move"
|
|
230
286
|
|
|
231
|
-
# The output location should not exist unless overwrite=True.
|
|
232
|
-
# Rather than use `exists()`, use os.stat since we might need
|
|
233
|
-
# the full answer later.
|
|
234
|
-
dest_stat: os.stat_result | None
|
|
235
|
-
try:
|
|
236
|
-
# Do not read through links of the file itself.
|
|
237
|
-
dest_stat = os.lstat(self.ospath)
|
|
238
|
-
except FileNotFoundError:
|
|
239
|
-
dest_stat = None
|
|
240
|
-
|
|
241
|
-
# It is possible that the source URI and target URI refer
|
|
242
|
-
# to the same file. This can happen for a number of reasons
|
|
243
|
-
# (such as soft links in the path, or they really are the same).
|
|
244
|
-
# In that case log a message and return as if the transfer
|
|
245
|
-
# completed (it technically did). A temporary file download
|
|
246
|
-
# can't be the same so the test can be skipped.
|
|
247
|
-
if dest_stat and not is_temporary:
|
|
248
|
-
# Be consistent and use lstat here (even though realpath
|
|
249
|
-
# has been called). It does not harm.
|
|
250
|
-
local_src_stat = os.lstat(local_src)
|
|
251
|
-
if dest_stat.st_ino == local_src_stat.st_ino and dest_stat.st_dev == local_src_stat.st_dev:
|
|
252
|
-
log.debug(
|
|
253
|
-
"Destination URI %s is the same file as source URI %s, returning immediately."
|
|
254
|
-
" No further action required.",
|
|
255
|
-
self,
|
|
256
|
-
local_uri,
|
|
257
|
-
)
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
if not overwrite and dest_stat:
|
|
261
|
-
raise FileExistsError(
|
|
262
|
-
f"Destination path '{self}' already exists. Transfer from {src} cannot be completed."
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
# Make the path absolute (but don't follow links since that
|
|
266
|
-
# would possibly cause us to end up in the wrong place if the
|
|
267
|
-
# file existed already as a soft link)
|
|
268
|
-
newFullPath = os.path.abspath(self.ospath)
|
|
269
|
-
outputDir = os.path.dirname(newFullPath)
|
|
270
287
|
if not os.path.isdir(outputDir):
|
|
271
288
|
# Must create the directory -- this can not be rolled back
|
|
272
289
|
# since another transfer running concurrently may
|
|
@@ -312,15 +329,38 @@ class FileResourcePath(ResourcePath):
|
|
|
312
329
|
# the same output directory. This at least guarantees that
|
|
313
330
|
# if multiple processes are writing to the same file
|
|
314
331
|
# simultaneously the file we end up with will not be corrupt.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
if overwrite:
|
|
333
|
+
with self.temporary_uri(prefix=self.parent(), suffix=self.getExtension()) as temp_copy:
|
|
334
|
+
shutil.copy(local_src, temp_copy.ospath)
|
|
335
|
+
with transaction.undoWith(f"copy from {local_src}", os.remove, newFullPath):
|
|
336
|
+
os.rename(temp_copy.ospath, newFullPath)
|
|
337
|
+
else:
|
|
338
|
+
# Create the file exclusively to ensure that no others are
|
|
339
|
+
# trying to write.
|
|
340
|
+
temp_path = newFullPath + ".transfer-tmp"
|
|
341
|
+
try:
|
|
342
|
+
with open(temp_path, "x"):
|
|
343
|
+
pass
|
|
344
|
+
except FileExistsError:
|
|
345
|
+
raise FileExistsError(
|
|
346
|
+
f"Another process is writing to '{self}'."
|
|
347
|
+
f" Transfer from {src} cannot be completed."
|
|
348
|
+
)
|
|
349
|
+
with transaction.undoWith(f"copy from {local_src}", os.remove, temp_path):
|
|
350
|
+
# Make sure file is writable, no matter the umask.
|
|
351
|
+
st = os.stat(temp_path)
|
|
352
|
+
os.chmod(temp_path, st.st_mode | stat.S_IWUSR)
|
|
353
|
+
shutil.copy(local_src, temp_path)
|
|
354
|
+
# Use link/remove to atomically and exclusively move the
|
|
355
|
+
# file into place (only one concurrent linker can win).
|
|
356
|
+
try:
|
|
357
|
+
os.link(temp_path, newFullPath)
|
|
358
|
+
except FileExistsError:
|
|
359
|
+
raise FileExistsError(
|
|
360
|
+
f"Another process wrote to '{self}'. Transfer from {src} cannot be completed."
|
|
361
|
+
)
|
|
362
|
+
finally:
|
|
363
|
+
os.remove(temp_path)
|
|
324
364
|
elif transfer == "link":
|
|
325
365
|
# Try hard link and if that fails use a symlink
|
|
326
366
|
with transaction.undoWith(f"link to {local_src}", os.remove, newFullPath):
|
lsst/resources/gs.py
CHANGED
|
@@ -202,17 +202,20 @@ class GSResourcePath(ResourcePath):
|
|
|
202
202
|
# Should this method do anything at all?
|
|
203
203
|
self.blob.upload_from_string(b"", retry=_RETRY_POLICY)
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
@contextlib.contextmanager
|
|
206
|
+
def _as_local(
|
|
207
|
+
self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
|
|
208
|
+
) -> Iterator[ResourcePath]:
|
|
206
209
|
with (
|
|
207
|
-
ResourcePath.temporary_uri(suffix=self.getExtension(), delete=
|
|
210
|
+
ResourcePath.temporary_uri(prefix=tmpdir, suffix=self.getExtension(), delete=True) as tmp_uri,
|
|
208
211
|
time_this(log, msg="Downloading %s to local file", args=(self,)),
|
|
209
212
|
):
|
|
210
213
|
try:
|
|
211
214
|
with tmp_uri.open("wb") as tmpFile:
|
|
212
215
|
self.blob.download_to_file(tmpFile, retry=_RETRY_POLICY)
|
|
216
|
+
yield tmp_uri
|
|
213
217
|
except NotFound as e:
|
|
214
218
|
raise FileNotFoundError(f"No such resource: {self}") from e
|
|
215
|
-
return tmp_uri.ospath, True
|
|
216
219
|
|
|
217
220
|
def transfer_from(
|
|
218
221
|
self,
|
|
@@ -220,6 +223,7 @@ class GSResourcePath(ResourcePath):
|
|
|
220
223
|
transfer: str = "copy",
|
|
221
224
|
overwrite: bool = False,
|
|
222
225
|
transaction: TransactionProtocol | None = None,
|
|
226
|
+
multithreaded: bool = True,
|
|
223
227
|
) -> None:
|
|
224
228
|
if transfer not in self.transferModes:
|
|
225
229
|
raise ValueError(f"Transfer mode '{transfer}' not supported by URI scheme {self.scheme}")
|
|
@@ -271,7 +275,10 @@ class GSResourcePath(ResourcePath):
|
|
|
271
275
|
break
|
|
272
276
|
else:
|
|
273
277
|
# Use local file and upload it
|
|
274
|
-
with
|
|
278
|
+
with (
|
|
279
|
+
src.as_local(multithreaded=multithreaded) as local_uri,
|
|
280
|
+
time_this(log, msg=timer_msg, args=timer_args),
|
|
281
|
+
):
|
|
275
282
|
self.blob.upload_from_filename(local_uri.ospath, retry=_RETRY_POLICY)
|
|
276
283
|
|
|
277
284
|
# This was an explicit move requested from a remote resource
|