lsst-resources 29.2025.2400__tar.gz → 29.2025.2500__tar.gz
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-29.2025.2400/python/lsst_resources.egg-info → lsst_resources-29.2025.2500}/PKG-INFO +1 -1
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/doc/lsst.resources/CHANGES.rst +31 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourcePath.py +8 -19
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/dav.py +25 -9
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/file.py +7 -6
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/gs.py +6 -3
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/http.py +21 -19
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/mem.py +7 -1
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/s3.py +14 -12
- lsst_resources-29.2025.2500/python/lsst/resources/version.py +2 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500/python/lsst_resources.egg-info}/PKG-INFO +1 -1
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_dav.py +6 -6
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_http.py +6 -6
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_location.py +5 -5
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_s3.py +11 -0
- lsst_resources-29.2025.2400/python/lsst/resources/version.py +0 -2
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/COPYRIGHT +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/LICENSE +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/MANIFEST.in +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/README.md +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/doc/lsst.resources/dav.rst +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/doc/lsst.resources/index.rst +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/doc/lsst.resources/internal-api.rst +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/doc/lsst.resources/s3.rst +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/pyproject.toml +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/__init__.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/__init__.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourceHandles/__init__.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourceHandles/_baseResourceHandle.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourceHandles/_davResourceHandle.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourceHandles/_fileResourceHandle.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourceHandles/_httpResourceHandle.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourceHandles/_s3ResourceHandle.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/davutils.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/location.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/packageresource.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/py.typed +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/s3utils.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/schemeless.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/tests.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/utils.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst_resources.egg-info/SOURCES.txt +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst_resources.egg-info/dependency_links.txt +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst_resources.egg-info/requires.txt +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst_resources.egg-info/top_level.txt +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst_resources.egg-info/zip-safe +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/setup.cfg +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_file.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_gs.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_mem.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_resource.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_s3utils.py +0 -0
- {lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/tests/test_schemeless.py +0 -0
{lsst_resources-29.2025.2400/python/lsst_resources.egg-info → lsst_resources-29.2025.2500}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-resources
|
|
3
|
-
Version: 29.2025.
|
|
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,3 +1,34 @@
|
|
|
1
|
+
Resources v29.1.0 (2025-06-13)
|
|
2
|
+
==============================
|
|
3
|
+
|
|
4
|
+
Miscellaneous Changes of Minor Interest
|
|
5
|
+
---------------------------------------
|
|
6
|
+
|
|
7
|
+
New Features
|
|
8
|
+
------------
|
|
9
|
+
|
|
10
|
+
- * Added ``ResourcePath.mtransfer()`` for doing multiple transfers in parallel.
|
|
11
|
+
The number of workers can be controlled using the ``$LSST_RESOURCES_NUM_WORKERS`` environment variable.
|
|
12
|
+
* ``transfer_from`` and ``as_local`` now have an additional parameter that can control whether implicit multithreading should be used for a single download.
|
|
13
|
+
* ``as_local`` has a new parameter that can be used to explicitly specify the local download location.
|
|
14
|
+
This can be used for ``transfer_from`` to allow the file to be downloaded to the local destination directory immediately. (`DM-31824 <https://rubinobs.atlassian.net/browse/DM-31824>`_)
|
|
15
|
+
- Added specialized support for schemes ``davs://`` and ``dav://`` hosted by storage endpoints implementing WebDAV protocol as described in `RFC-4918 HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV) <http://www.webdav.org/specs/rfc4918.html>`_. (`DM-49784 <https://rubinobs.atlassian.net/browse/DM-49784>`_)
|
|
16
|
+
- Added new bulk removal API: ``ResourcePath.mremove()``.
|
|
17
|
+
This can be 10 times faster than calling ``remove()`` in a loop. (`DM-50724 <https://rubinobs.atlassian.net/browse/DM-50724>`_)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Miscellaneous Changes of Minor Interest
|
|
21
|
+
---------------------------------------
|
|
22
|
+
|
|
23
|
+
- It is now possible to control how bulk APIs such as ``mexists()`` and ``mtransfer()`` work.
|
|
24
|
+
Added ``$LSST_RESOURCES_NUM_WORKERS`` environment variable to specify how many workers should be used.
|
|
25
|
+
The default is derived from the number of CPUs but capped at 10.
|
|
26
|
+
Also the ``mexists()`` method has an explicit parameter to allow the number of workers to be specified.
|
|
27
|
+
Added ``$LSST_RESOURCES_EXECUTOR`` to specify how the jobs should be executed.
|
|
28
|
+
The default is ``threads`` (which is the same as used previously) but on Linux more performance may be achievable by setting this environment variable to ``process``. (`DM-50074 <https://rubinobs.atlassian.net/browse/DM-50074>`_)
|
|
29
|
+
- * Fixed problem with multiple ``flush()`` calls with S3 resource handle for small chunks.
|
|
30
|
+
* Fixed bug in File resource handle where ``flush()`` was mistakenly calling ``close()``. (`DM-51087 <https://rubinobs.atlassian.net/browse/DM-51087>`_)
|
|
31
|
+
|
|
1
32
|
Resources v29.0.0 (2025-03-25)
|
|
2
33
|
==============================
|
|
3
34
|
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/_resourcePath.py
RENAMED
|
@@ -1255,7 +1255,10 @@ class ResourcePath: # numpydoc ignore=PR02
|
|
|
1255
1255
|
"""
|
|
1256
1256
|
return self
|
|
1257
1257
|
|
|
1258
|
-
|
|
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
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
|
@@ -79,7 +79,10 @@ class FileResourcePath(ResourcePath):
|
|
|
79
79
|
"""Remove the resource."""
|
|
80
80
|
os.remove(self.ospath)
|
|
81
81
|
|
|
82
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
+
yield self
|
|
102
103
|
|
|
103
104
|
def read(self, size: int = -1) -> bytes:
|
|
104
105
|
with open(self.ospath, "rb") as fh:
|
|
@@ -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(prefix=tmpdir, 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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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=
|
|
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
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
-
|
|
1604
|
+
yield tmp_uri
|
|
1603
1605
|
|
|
1604
1606
|
def _send_webdav_request(
|
|
1605
1607
|
self,
|
|
@@ -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
|
-
|
|
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}'")
|
|
@@ -477,7 +477,10 @@ class S3ResourcePath(ResourcePath):
|
|
|
477
477
|
|
|
478
478
|
return s3, f"{self._bucket}/{self.relativeToPathRoot}"
|
|
479
479
|
|
|
480
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
#
|
|
615
|
-
#
|
|
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-29.2025.2400 → lsst_resources-29.2025.2500/python/lsst_resources.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-resources
|
|
3
|
-
Version: 29.2025.
|
|
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
|
|
@@ -284,12 +284,12 @@ class DavReadWriteTestCase(GenericReadWriteTestCase, unittest.TestCase):
|
|
|
284
284
|
self.assertTrue(remote_file.exists())
|
|
285
285
|
self.assertEqual(remote_file.size(), len(contents))
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
287
|
+
with remote_file._as_local() as local_uri:
|
|
288
|
+
self.assertTrue(local_uri.isTemporary)
|
|
289
|
+
self.assertTrue(os.path.exists(local_uri.ospath))
|
|
290
|
+
self.assertTrue(os.stat(local_uri.ospath).st_size, len(contents))
|
|
291
|
+
self.assertEqual(local_uri.read(), contents)
|
|
292
|
+
self.assertFalse(local_uri.exists())
|
|
293
293
|
|
|
294
294
|
def test_dav_size(self):
|
|
295
295
|
# Retrieving the size of a non-existent file must raise.
|
|
@@ -315,12 +315,12 @@ class HttpReadWriteWebdavTestCase(GenericReadWriteTestCase, unittest.TestCase):
|
|
|
315
315
|
remote_file = self.tmpdir.join(self._get_file_name())
|
|
316
316
|
self.assertIsNone(remote_file.write(data=contents, overwrite=True))
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
318
|
+
with remote_file._as_local() as local_uri:
|
|
319
|
+
self.assertTrue(local_uri.isTemporary)
|
|
320
|
+
self.assertTrue(os.path.exists(local_uri.ospath))
|
|
321
|
+
self.assertTrue(os.stat(local_uri.ospath).st_size, len(contents))
|
|
322
|
+
self.assertEqual(local_uri.read(), contents)
|
|
323
|
+
self.assertFalse(local_uri.exists())
|
|
324
324
|
|
|
325
325
|
def test_dav_size(self):
|
|
326
326
|
# Size of a non-existent file must raise.
|
|
@@ -98,7 +98,7 @@ class LocationTestCase(unittest.TestCase):
|
|
|
98
98
|
|
|
99
99
|
for uriInfo in uriStrings:
|
|
100
100
|
uri = ResourcePath(uriInfo[0], root=testRoot, forceAbsolute=uriInfo[1], forceDirectory=uriInfo[2])
|
|
101
|
-
with self.subTest(in_uri=uriInfo[0], out_uri=uri):
|
|
101
|
+
with self.subTest(in_uri=repr(uriInfo[0]), out_uri=repr(uri)):
|
|
102
102
|
self.assertEqual(uri.scheme, uriInfo[3], "test scheme")
|
|
103
103
|
self.assertEqual(uri.netloc, uriInfo[4], "test netloc")
|
|
104
104
|
self.assertEqual(uri.path, uriInfo[5], "test path")
|
|
@@ -115,7 +115,7 @@ class LocationTestCase(unittest.TestCase):
|
|
|
115
115
|
|
|
116
116
|
for uriInfo in uriStrings:
|
|
117
117
|
uri = ResourcePath(uriInfo[0], forceAbsolute=uriInfo[1], forceDirectory=uriInfo[2])
|
|
118
|
-
with self.subTest(in_uri=uriInfo[0], out_uri=uri):
|
|
118
|
+
with self.subTest(in_uri=repr(uriInfo[0]), out_uri=repr(uri)):
|
|
119
119
|
self.assertEqual(uri.scheme, uriInfo[3], "test scheme")
|
|
120
120
|
self.assertEqual(uri.netloc, uriInfo[4], "test netloc")
|
|
121
121
|
# Use ospath here to ensure that we have unquoted any
|
|
@@ -133,7 +133,7 @@ class LocationTestCase(unittest.TestCase):
|
|
|
133
133
|
|
|
134
134
|
for uriInfo in uriStrings:
|
|
135
135
|
uri = ResourcePath(uriInfo[0], forceAbsolute=False).updatedFile(uriInfo[1])
|
|
136
|
-
with self.subTest(in_uri=uriInfo[0], out_uri=uri):
|
|
136
|
+
with self.subTest(in_uri=repr(uriInfo[0]), out_uri=repr(uri)):
|
|
137
137
|
self.assertEqual(uri.path, uriInfo[2])
|
|
138
138
|
|
|
139
139
|
# Check that schemeless can become file scheme.
|
|
@@ -333,7 +333,7 @@ class LocationTestCase(unittest.TestCase):
|
|
|
333
333
|
"""Test round tripping of the posix to os.path conversion helpers."""
|
|
334
334
|
testPaths = ("/a/b/c.e", "a/b", "a/b/", "/a/b", "/a/b/", "a/b/c.e")
|
|
335
335
|
for p in testPaths:
|
|
336
|
-
with self.subTest(path=p):
|
|
336
|
+
with self.subTest(path=repr(p)):
|
|
337
337
|
self.assertEqual(os2posix(posix2os(p)), p)
|
|
338
338
|
|
|
339
339
|
def testSplit(self):
|
|
@@ -368,7 +368,7 @@ class LocationTestCase(unittest.TestCase):
|
|
|
368
368
|
)
|
|
369
369
|
|
|
370
370
|
for p, e in zip(testPaths, expected, strict=True):
|
|
371
|
-
with self.subTest(path=p):
|
|
371
|
+
with self.subTest(path=repr(p)):
|
|
372
372
|
uri = ResourcePath(p, testRoot)
|
|
373
373
|
head, tail = uri.split()
|
|
374
374
|
self.assertEqual((head.geturl(), tail), e)
|
|
@@ -351,6 +351,17 @@ class S3WithProfileReadWriteTestCase(S3ReadWriteTestCaseBase, unittest.TestCase)
|
|
|
351
351
|
self.assertEqual(path2._bucket, "ceph:bucket2")
|
|
352
352
|
self.assertIsNone(path2._profile)
|
|
353
353
|
|
|
354
|
+
def test_transfer_from_different_endpoints(self):
|
|
355
|
+
# Create a bucket using a different endpoint (the default endpoint.)
|
|
356
|
+
boto3.resource("s3").create_bucket(Bucket="source-bucket")
|
|
357
|
+
source_path = ResourcePath("s3://source-bucket/file.txt")
|
|
358
|
+
source_path.write(b"123")
|
|
359
|
+
target_path = ResourcePath(f"s3://{self.netloc}/target.txt")
|
|
360
|
+
# Transfer from default endpoint to custom endpoint with custom
|
|
361
|
+
# profile.
|
|
362
|
+
target_path.transfer_from(source_path)
|
|
363
|
+
self.assertEqual(target_path.read(), b"123")
|
|
364
|
+
|
|
354
365
|
|
|
355
366
|
if __name__ == "__main__":
|
|
356
367
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/doc/lsst.resources/internal-api.rst
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/davutils.py
RENAMED
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/location.py
RENAMED
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/packageresource.py
RENAMED
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/s3utils.py
RENAMED
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst/resources/schemeless.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2400 → lsst_resources-29.2025.2500}/python/lsst_resources.egg-info/zip-safe
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|