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/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
- def _as_local(self) -> tuple[str, bool]:
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
- path : `str`
90
- The local path to this file.
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
- return self.ospath, self.isTemporary
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
- with src.as_local() as local_uri:
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
- with self.temporary_uri(prefix=self.parent(), suffix=self.getExtension()) as temp_copy:
316
- shutil.copy(local_src, temp_copy.ospath)
317
- with transaction.undoWith(f"copy from {local_src}", os.remove, newFullPath):
318
- # os.rename works even if the file exists.
319
- # It's possible that another process has copied a file
320
- # in whilst this one was copying. If overwrite
321
- # protection is needed then another stat() call should
322
- # happen here.
323
- os.rename(temp_copy.ospath, newFullPath)
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
- def _as_local(self) -> tuple[str, bool]:
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=False) as tmp_uri,
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 src.as_local() as local_uri, time_this(log, msg=timer_msg, args=timer_args):
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