lsst-resources 29.2025.2400__py3-none-any.whl → 29.2025.2500__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.
@@ -1255,7 +1255,10 @@ class ResourcePath: # numpydoc ignore=PR02
1255
1255
  """
1256
1256
  return self
1257
1257
 
1258
- def _as_local(self, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> tuple[str, bool]:
1258
+ @contextlib.contextmanager
1259
+ def _as_local(
1260
+ self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
1261
+ ) -> Iterator[ResourcePath]:
1259
1262
  """Return the location of the (possibly remote) resource as local file.
1260
1263
 
1261
1264
  This is a helper function for `as_local` context manager.
@@ -1274,13 +1277,9 @@ class ResourcePath: # numpydoc ignore=PR02
1274
1277
 
1275
1278
  Returns
1276
1279
  -------
1277
- path : `str`
1278
- If this is a remote resource, it will be a copy of the resource
1279
- on the local file system, probably in a temporary directory.
1280
- For a local resource this should be the actual path to the
1281
- resource.
1282
- is_temporary : `bool`
1283
- Indicates if the local path is a temporary file or not.
1280
+ local_uri : `ResourcePath`
1281
+ A URI to a local POSIX file. This can either be the same resource
1282
+ or a local downloaded copy of the resource.
1284
1283
  """
1285
1284
  raise NotImplementedError()
1286
1285
 
@@ -1330,18 +1329,8 @@ class ResourcePath: # numpydoc ignore=PR02
1330
1329
  temp_dir = ResourcePath(tmpdir, forceDirectory=True) if tmpdir is not None else None
1331
1330
  if temp_dir is not None and not temp_dir.isLocal:
1332
1331
  raise ValueError(f"Temporary directory for as_local must be local resource not {temp_dir}")
1333
- local_src, is_temporary = self._as_local(multithreaded=multithreaded, tmpdir=temp_dir)
1334
- local_uri = ResourcePath(local_src, isTemporary=is_temporary)
1335
-
1336
- try:
1332
+ with self._as_local(multithreaded=multithreaded, tmpdir=temp_dir) as local_uri:
1337
1333
  yield local_uri
1338
- finally:
1339
- # The caller might have relocated the temporary file.
1340
- # Do not ever delete if the temporary matches self
1341
- # (since it may have been that a temporary file was made local
1342
- # but already was local).
1343
- if self != local_uri and is_temporary and local_uri.exists():
1344
- local_uri.remove()
1345
1334
 
1346
1335
  @classmethod
1347
1336
  @contextlib.contextmanager
lsst/resources/dav.py CHANGED
@@ -172,7 +172,21 @@ dav_globals: DavGlobals = DavGlobals()
172
172
 
173
173
 
174
174
  class DavResourcePath(ResourcePath):
175
- """WebDAV resource."""
175
+ """WebDAV resource.
176
+
177
+ Parameters
178
+ ----------
179
+ uri : `ResourcePathExpression`
180
+ URI to store in object.
181
+ root : `str` or `ResourcePath` or `None`, optional
182
+ Root for relative URIs. Not used in this constructor.
183
+ forceAbsolute : `bool`
184
+ Whether to force absolute URI. A WebDAV URI is always absolute.
185
+ forceDirectory : `bool` or `None`, optional
186
+ Whether this URI represents a directory.
187
+ isTemporary : `bool` or `None`, optional
188
+ Whether this URI represents a temporary resource.
189
+ """
176
190
 
177
191
  def __init__(
178
192
  self,
@@ -382,7 +396,10 @@ class DavResourcePath(ResourcePath):
382
396
  headers.update({"Accept-Encoding": "identity"})
383
397
  return self._client.read_range(self._internal_url, start=start, end=end, headers=headers)
384
398
 
385
- def _as_local(self, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> tuple[str, bool]:
399
+ @contextlib.contextmanager
400
+ def _as_local(
401
+ self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
402
+ ) -> Iterator[ResourcePath]:
386
403
  """Download object and place in temporary directory.
387
404
 
388
405
  Parameters
@@ -399,10 +416,9 @@ class DavResourcePath(ResourcePath):
399
416
 
400
417
  Returns
401
418
  -------
402
- path : `str`
403
- Path to local temporary file.
404
- temporary : `bool`
405
- Always returns `True`. This is always a temporary file.
419
+ local_uri : `ResourcePath`
420
+ A URI to a local POSIX file corresponding to a local temporary
421
+ downloaded copy of the resource.
406
422
  """
407
423
  # We need to ensure that this resource is actually a file. dCache
408
424
  # responds with a HTML-formatted content to a HTTP GET request to a
@@ -417,9 +433,9 @@ class DavResourcePath(ResourcePath):
417
433
  else:
418
434
  buffer_size = _calc_tmpdir_buffer_size(tmpdir.ospath)
419
435
 
420
- with ResourcePath.temporary_uri(suffix=self.getExtension(), prefix=tmpdir, delete=False) as tmp_uri:
436
+ with ResourcePath.temporary_uri(suffix=self.getExtension(), prefix=tmpdir, delete=True) as tmp_uri:
421
437
  self._client.download(self._internal_url, tmp_uri.ospath, buffer_size)
422
- return tmp_uri.ospath, True
438
+ yield tmp_uri
423
439
 
424
440
  def write(self, data: BinaryIO | bytes, overwrite: bool = True) -> None:
425
441
  """Write the supplied bytes to the new resource.
@@ -470,7 +486,7 @@ class DavResourcePath(ResourcePath):
470
486
 
471
487
  Parameters
472
488
  ----------
473
- recursive: `bool`
489
+ recursive : `bool`
474
490
  If `True` recursively remove all files and directories under this
475
491
  directory.
476
492
 
lsst/resources/file.py CHANGED
@@ -79,7 +79,10 @@ class FileResourcePath(ResourcePath):
79
79
  """Remove the resource."""
80
80
  os.remove(self.ospath)
81
81
 
82
- def _as_local(self, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> tuple[str, bool]:
82
+ @contextlib.contextmanager
83
+ def _as_local(
84
+ self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
85
+ ) -> Iterator[ResourcePath]:
83
86
  """Return the local path of the file.
84
87
 
85
88
  This is an internal helper for ``as_local()``.
@@ -93,12 +96,10 @@ class FileResourcePath(ResourcePath):
93
96
 
94
97
  Returns
95
98
  -------
96
- path : `str`
97
- The local path to this file.
98
- temporary : `bool`
99
- Always returns the temporary nature of the input file resource.
99
+ local_uri : `ResourcePath`
100
+ A local URI. In this case it will be itself.
100
101
  """
101
- return self.ospath, self.isTemporary
102
+ yield self
102
103
 
103
104
  def read(self, size: int = -1) -> bytes:
104
105
  with open(self.ospath, "rb") as fh:
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, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> 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(prefix=tmpdir, 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,
lsst/resources/http.py CHANGED
@@ -1531,7 +1531,10 @@ class HttpResourcePath(ResourcePath):
1531
1531
  except json.JSONDecodeError:
1532
1532
  raise ValueError(f"could not deserialize response to POST request for URL {self}")
1533
1533
 
1534
- def _as_local(self, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> tuple[str, bool]:
1534
+ @contextlib.contextmanager
1535
+ def _as_local(
1536
+ self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
1537
+ ) -> Iterator[ResourcePath]:
1535
1538
  """Download object over HTTP and place in temporary directory.
1536
1539
 
1537
1540
  Parameters
@@ -1548,10 +1551,9 @@ class HttpResourcePath(ResourcePath):
1548
1551
 
1549
1552
  Returns
1550
1553
  -------
1551
- path : `str`
1552
- Path to local temporary file.
1553
- temporary : `bool`
1554
- Always returns `True`. This is always a temporary file.
1554
+ local_uri : `ResourcePath`
1555
+ A URI to a local POSIX file corresponding to a local temporary
1556
+ downloaded copy of the resource.
1555
1557
  """
1556
1558
  # Use the session as a context manager to ensure that connections
1557
1559
  # to both the front end and back end servers are closed after the
@@ -1570,7 +1572,7 @@ class HttpResourcePath(ResourcePath):
1570
1572
  buffer_size = _calc_tmpdir_buffer_size(tmpdir.ospath)
1571
1573
 
1572
1574
  with ResourcePath.temporary_uri(
1573
- suffix=self.getExtension(), prefix=tmpdir, delete=False
1575
+ suffix=self.getExtension(), prefix=tmpdir, delete=True
1574
1576
  ) as tmp_uri:
1575
1577
  expected_length = int(resp.headers.get("Content-Length", "-1"))
1576
1578
  with time_this(
@@ -1586,20 +1588,20 @@ class HttpResourcePath(ResourcePath):
1586
1588
  tmpFile.write(chunk)
1587
1589
  content_length += len(chunk)
1588
1590
 
1589
- # Check that the expected and actual content lengths match. Perform
1590
- # this check only when the contents of the file was not encoded by
1591
- # the server.
1592
- if (
1593
- "Content-Encoding" not in resp.headers
1594
- and expected_length >= 0
1595
- and expected_length != content_length
1596
- ):
1597
- raise ValueError(
1598
- f"Size of downloaded file does not match value in Content-Length header for {self}: "
1599
- f"expecting {expected_length} and got {content_length} bytes"
1600
- )
1591
+ # Check that the expected and actual content lengths match.
1592
+ # Perform this check only when the contents of the file was not
1593
+ # encoded by the server.
1594
+ if (
1595
+ "Content-Encoding" not in resp.headers
1596
+ and expected_length >= 0
1597
+ and expected_length != content_length
1598
+ ):
1599
+ raise ValueError(
1600
+ f"Size of downloaded file does not match value in Content-Length header for {self}: "
1601
+ f"expecting {expected_length} and got {content_length} bytes"
1602
+ )
1601
1603
 
1602
- return tmpFile.name, True
1604
+ yield tmp_uri
1603
1605
 
1604
1606
  def _send_webdav_request(
1605
1607
  self,
lsst/resources/mem.py CHANGED
@@ -13,6 +13,9 @@ from __future__ import annotations
13
13
 
14
14
  __all__ = ("InMemoryResourcePath",)
15
15
 
16
+ import contextlib
17
+ from collections.abc import Iterator
18
+
16
19
  from ._resourcePath import ResourcePath
17
20
 
18
21
 
@@ -27,5 +30,8 @@ class InMemoryResourcePath(ResourcePath):
27
30
  """Test for existence and always return False."""
28
31
  return True
29
32
 
30
- def _as_local(self, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> tuple[str, bool]:
33
+ @contextlib.contextmanager
34
+ def _as_local(
35
+ self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
36
+ ) -> Iterator[ResourcePath]:
31
37
  raise RuntimeError(f"Do not know how to retrieve data for URI '{self}'")
lsst/resources/s3.py CHANGED
@@ -477,7 +477,10 @@ class S3ResourcePath(ResourcePath):
477
477
 
478
478
  return s3, f"{self._bucket}/{self.relativeToPathRoot}"
479
479
 
480
- def _as_local(self, multithreaded: bool = True, tmpdir: ResourcePath | None = None) -> tuple[str, bool]:
480
+ @contextlib.contextmanager
481
+ def _as_local(
482
+ self, multithreaded: bool = True, tmpdir: ResourcePath | None = None
483
+ ) -> Iterator[ResourcePath]:
481
484
  """Download object from S3 and place in temporary directory.
482
485
 
483
486
  Parameters
@@ -494,13 +497,12 @@ class S3ResourcePath(ResourcePath):
494
497
 
495
498
  Returns
496
499
  -------
497
- path : `str`
498
- Path to local temporary file.
499
- temporary : `bool`
500
- Always returns `True`. This is always a temporary file.
500
+ local_uri : `ResourcePath`
501
+ A URI to a local POSIX file corresponding to a local temporary
502
+ downloaded copy of the resource.
501
503
  """
502
504
  with (
503
- ResourcePath.temporary_uri(prefix=tmpdir, suffix=self.getExtension(), delete=False) as tmp_uri,
505
+ ResourcePath.temporary_uri(prefix=tmpdir, suffix=self.getExtension(), delete=True) as tmp_uri,
504
506
  self._use_threads_temp_override(multithreaded),
505
507
  time_this(log, msg="Downloading %s to local file", args=(self,)),
506
508
  ):
@@ -511,7 +513,7 @@ class S3ResourcePath(ResourcePath):
511
513
  )
512
514
  with tmp_uri.open("wb") as tmpFile:
513
515
  self._download_file(tmpFile, progress)
514
- return tmp_uri.ospath, True
516
+ yield tmp_uri
515
517
 
516
518
  @backoff.on_exception(backoff.expo, all_retryable_errors, max_time=max_retry_time)
517
519
  def _upload_file(self, local_file: ResourcePath, progress: ProgressPercentage | None) -> None:
@@ -542,7 +544,7 @@ class S3ResourcePath(ResourcePath):
542
544
  try:
543
545
  self.client.copy_object(CopySource=copy_source, Bucket=self._bucket, Key=self.relativeToPathRoot)
544
546
  except (self.client.exceptions.NoSuchKey, self.client.exceptions.NoSuchBucket) as err:
545
- raise FileNotFoundError(f"No such resource to transfer: {self}") from err
547
+ raise FileNotFoundError(f"No such resource to transfer: {src} -> {self}") from err
546
548
  except ClientError as err:
547
549
  translate_client_error(err, self)
548
550
  raise
@@ -609,10 +611,10 @@ class S3ResourcePath(ResourcePath):
609
611
  timer_msg = "Transfer from %s to %s"
610
612
  timer_args = (src, self)
611
613
 
612
- if isinstance(src, type(self)):
613
- # Looks like an S3 remote uri so we can use direct copy
614
- # note that boto3.resource.meta.copy is cleverer than the low
615
- # level copy_object
614
+ if isinstance(src, type(self)) and self.client == src.client:
615
+ # Looks like an S3 remote uri so we can use direct copy.
616
+ # This only works if the source and destination are using the same
617
+ # S3 endpoint and profile.
616
618
  with time_this(log, msg=timer_msg, args=timer_args):
617
619
  self._copy_from(src)
618
620
 
lsst/resources/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.2400"
2
+ __version__ = "29.2025.2500"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-resources
3
- Version: 29.2025.2400
3
+ Version: 29.2025.2500
4
4
  Summary: An abstraction layer for reading and writing from URI file resources.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
6
  License: BSD 3-Clause License
@@ -1,31 +1,31 @@
1
1
  lsst/__init__.py,sha256=9I6UQ9gj-ZcPlvsa0OPBo76UujxXVehVzw9yMAOQvyM,466
2
2
  lsst/resources/__init__.py,sha256=BDj6uokvd0ZQNGl-Xgz5gZd83Z0L2gFqGSk0KJpylP8,778
3
- lsst/resources/_resourcePath.py,sha256=NLjOjz0ARFJxFwE9fw4HowFV0FrdYBX_jiyXODPFSVY,74273
4
- lsst/resources/dav.py,sha256=XQMAZgthvtJ4hUXZ0avBAHnAYjMYU3m2DPHDyQVT7a8,31547
3
+ lsst/resources/_resourcePath.py,sha256=oimnY99a8BBMKahtQxlsGl29fNsM1w5_IZ9mBpbRgzs,73673
4
+ lsst/resources/dav.py,sha256=ZYP7PnQzS7epm5woxnn1_t1XhsPQZm6_q1kv8baUfn4,32100
5
5
  lsst/resources/davutils.py,sha256=xALuMRSvYroqn_Jz6bjnj43b4OgOgCJtNW49kyTtuiw,97983
6
- lsst/resources/file.py,sha256=-jPuoHvTEtx5tnDyNkfwhWAyX0cTwkuMd-JvJn9EGdE,23226
7
- lsst/resources/gs.py,sha256=Lpo5GAzH7R7HG8E5RMGOdP4j4hjWJn-k6M3OXj0nHQM,12783
8
- lsst/resources/http.py,sha256=gQNlmCyx9z_0GwrfBaOQ4WfV3Psz6BTfJYdTyolIntA,92441
6
+ lsst/resources/file.py,sha256=ygDUVZyKROKtTrGh9KZv4tD3uhlhiHmKOizY61UILJw,23178
7
+ lsst/resources/gs.py,sha256=3qMEqO1wIK05BJmuUHtsEunuYWgR4-eB5Z3ffxEtb0o,12827
8
+ lsst/resources/http.py,sha256=B-QG9zYFey1u5KOmqdBrRjeKem9eYaOdR4MZTLJloPw,92528
9
9
  lsst/resources/location.py,sha256=x3Tq0x5o1OXYmZDxYBenUG1N71wtDhnjVAr3s2ZEiu8,7937
10
- lsst/resources/mem.py,sha256=VOWh7XxJPfqKcFdLZSjKEAfORQ2AHZHpxmjT8LniV60,1008
10
+ lsst/resources/mem.py,sha256=xCpGgvxF2gmO5gLkOikKvIet2RPvaPCiARenR9pUWCk,1115
11
11
  lsst/resources/packageresource.py,sha256=vnfeRlpVwpC5cDQZE6Lnh8EH6oZy1sH2vLz9ONYjJ4k,6817
12
12
  lsst/resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- lsst/resources/s3.py,sha256=u2rZkg12C3JgIehIJOx69NeipdN6fawhvdZKffhdAF8,29573
13
+ lsst/resources/s3.py,sha256=NGJPM4BjtqFIPvg9vbp_blrIRt009NbOm06cr65Wqmw,29662
14
14
  lsst/resources/s3utils.py,sha256=ojWf9BPrK9mhGQ8jvs4_8Nsqf9360e79U5FnPTxe24A,14576
15
15
  lsst/resources/schemeless.py,sha256=GfJcKzZ0XIeepfQdW4HPZWiZlSp_ej0SEtSiJTrDUQs,10666
16
16
  lsst/resources/tests.py,sha256=G43eaajzIaA6D-zz63vsgYJTeSy9Bso-my_Dh4X34CE,44966
17
17
  lsst/resources/utils.py,sha256=6O3Mq7JbPEtqyD2lM77pRpwcPMfV5SxiNMknw-F2vNs,8097
18
- lsst/resources/version.py,sha256=AyivLkwA4FcYO6eiFsfTA_EuqMhydgvzBupNZlQZL-E,55
18
+ lsst/resources/version.py,sha256=uW49-HYTP9jPdj_-PD584MYKQsfoumQbft7BWRqu6H4,55
19
19
  lsst/resources/_resourceHandles/__init__.py,sha256=zOcZ8gVEBdAWcHJaZabA8Vdq-wAVcxjbmA_1b1IWM6M,76
20
20
  lsst/resources/_resourceHandles/_baseResourceHandle.py,sha256=lQwxDOmFUNJndTxsjpz-HxrQBL0L-z4aXQocHdOEI7c,4676
21
21
  lsst/resources/_resourceHandles/_davResourceHandle.py,sha256=12X5-K5KqzG4EV78ZkIIrjcZcFroXy3Y2JQ_N-SDqF0,6616
22
22
  lsst/resources/_resourceHandles/_fileResourceHandle.py,sha256=2nC8tfP_ynAfjpzrtkw_1ahx1CuMEFpZ5mLmofSShUk,3676
23
23
  lsst/resources/_resourceHandles/_httpResourceHandle.py,sha256=Yami8IVGeru4bLQCag-OvGG0ltz1qyEg57FY4IEB87Y,10995
24
24
  lsst/resources/_resourceHandles/_s3ResourceHandle.py,sha256=Cp-eBtptskbmthy3DwLKPpYPLvU_lrqtK10X37inHt0,12406
25
- lsst_resources-29.2025.2400.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
26
- lsst_resources-29.2025.2400.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
27
- lsst_resources-29.2025.2400.dist-info/METADATA,sha256=ba8Zw1ARVEELnFMBRB-4pn_WOBtDdS-7GKIqlONe-yQ,2237
28
- lsst_resources-29.2025.2400.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
- lsst_resources-29.2025.2400.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
30
- lsst_resources-29.2025.2400.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
- lsst_resources-29.2025.2400.dist-info/RECORD,,
25
+ lsst_resources-29.2025.2500.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
26
+ lsst_resources-29.2025.2500.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
27
+ lsst_resources-29.2025.2500.dist-info/METADATA,sha256=iqqJZGYMm7cHOH8InmjiEHuerf4G6dbuNOclfcZOyB0,2237
28
+ lsst_resources-29.2025.2500.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ lsst_resources-29.2025.2500.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
30
+ lsst_resources-29.2025.2500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
+ lsst_resources-29.2025.2500.dist-info/RECORD,,