lsst-resources 29.2025.2100__tar.gz → 29.2025.2400__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.2100/python/lsst_resources.egg-info → lsst_resources-29.2025.2400}/PKG-INFO +1 -1
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourceHandles/_fileResourceHandle.py +1 -1
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourceHandles/_s3ResourceHandle.py +3 -17
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourcePath.py +9 -1
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/http.py +152 -11
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/s3.py +1 -1
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/tests.py +13 -3
- lsst_resources-29.2025.2400/python/lsst/resources/version.py +2 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400/python/lsst_resources.egg-info}/PKG-INFO +1 -1
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_http.py +35 -0
- lsst_resources-29.2025.2100/python/lsst/resources/version.py +0 -2
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/COPYRIGHT +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/LICENSE +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/MANIFEST.in +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/README.md +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/doc/lsst.resources/CHANGES.rst +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/doc/lsst.resources/dav.rst +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/doc/lsst.resources/index.rst +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/doc/lsst.resources/internal-api.rst +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/doc/lsst.resources/s3.rst +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/pyproject.toml +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/__init__.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/__init__.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourceHandles/__init__.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourceHandles/_baseResourceHandle.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourceHandles/_davResourceHandle.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourceHandles/_httpResourceHandle.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/dav.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/davutils.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/file.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/gs.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/location.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/mem.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/packageresource.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/py.typed +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/s3utils.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/schemeless.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/utils.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst_resources.egg-info/SOURCES.txt +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst_resources.egg-info/dependency_links.txt +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst_resources.egg-info/requires.txt +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst_resources.egg-info/top_level.txt +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst_resources.egg-info/zip-safe +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/setup.cfg +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_dav.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_file.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_gs.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_location.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_mem.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_resource.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_s3.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_s3utils.py +0 -0
- {lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/tests/test_schemeless.py +0 -0
{lsst_resources-29.2025.2100/python/lsst_resources.egg-info → lsst_resources-29.2025.2400}/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.2400
|
|
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
|
|
@@ -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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/_resourcePath.py
RENAMED
|
@@ -649,9 +649,11 @@ class ResourcePath: # numpydoc ignore=PR02
|
|
|
649
649
|
# Disallow a change in scheme
|
|
650
650
|
if "scheme" in kwargs:
|
|
651
651
|
raise ValueError(f"Can not use replace() method to change URI scheme for {self}")
|
|
652
|
-
|
|
652
|
+
result = self.__class__(
|
|
653
653
|
self._uri._replace(**kwargs), forceDirectory=forceDirectory, isTemporary=isTemporary
|
|
654
654
|
)
|
|
655
|
+
result._copy_extra_attributes(self)
|
|
656
|
+
return result
|
|
655
657
|
|
|
656
658
|
def updatedFile(self, newfile: str) -> ResourcePath:
|
|
657
659
|
"""Return new URI with an updated final component of the path.
|
|
@@ -1903,6 +1905,12 @@ class ResourcePath: # numpydoc ignore=PR02
|
|
|
1903
1905
|
"""
|
|
1904
1906
|
raise NotImplementedError(f"URL signing is not supported for '{self.scheme}'")
|
|
1905
1907
|
|
|
1908
|
+
def _copy_extra_attributes(self, original_uri: ResourcePath) -> None:
|
|
1909
|
+
# May be overridden by subclasses to transfer attributes when a
|
|
1910
|
+
# ResourcePath is constructed using the "clone" version of the
|
|
1911
|
+
# ResourcePath constructor by passing in a ResourcePath object.
|
|
1912
|
+
pass
|
|
1913
|
+
|
|
1906
1914
|
|
|
1907
1915
|
ResourcePathExpression = str | urllib.parse.ParseResult | ResourcePath | Path
|
|
1908
1916
|
"""Type-annotation alias for objects that can be coerced to ResourcePath.
|
|
@@ -759,6 +759,42 @@ class HttpResourcePath(ResourcePath):
|
|
|
759
759
|
a HTTP URL. The value of the variable is not inspected.
|
|
760
760
|
"""
|
|
761
761
|
|
|
762
|
+
@staticmethod
|
|
763
|
+
def create_http_resource_path(
|
|
764
|
+
path: str, *, extra_headers: dict[str, str] | None = None
|
|
765
|
+
) -> HttpResourcePath:
|
|
766
|
+
"""Create an instance of `HttpResourcePath` with additional
|
|
767
|
+
HTTP-specific configuration.
|
|
768
|
+
|
|
769
|
+
Parameters
|
|
770
|
+
----------
|
|
771
|
+
path : `str`
|
|
772
|
+
HTTP URL to be wrapped in a `ResourcePath` instance.
|
|
773
|
+
extra_headers : `dict` [ `str`, `str` ], optional
|
|
774
|
+
Additional headers that will be sent with every HTTP request made
|
|
775
|
+
by this `ResourcePath`. These override any headers that may be
|
|
776
|
+
generated internally by `HttpResourcePath` (e.g. authentication
|
|
777
|
+
headers).
|
|
778
|
+
|
|
779
|
+
Return
|
|
780
|
+
------
|
|
781
|
+
instance : `ResourcePath`
|
|
782
|
+
Newly-created `HttpResourcePath` instance.
|
|
783
|
+
|
|
784
|
+
Notes
|
|
785
|
+
-----
|
|
786
|
+
Most users should use the `ResourcePath` constructor, instead.
|
|
787
|
+
"""
|
|
788
|
+
# Make sure we instantiate ResourcePath using a string to guarantee we
|
|
789
|
+
# get a new ResourcePath. If we accidentally provided a ResourcePath
|
|
790
|
+
# instance instead, the ResourcePath constructor sometimes returns
|
|
791
|
+
# the original object and we would be modifying an object that is
|
|
792
|
+
# supposed to be immutable.
|
|
793
|
+
instance = ResourcePath(str(path))
|
|
794
|
+
assert isinstance(instance, HttpResourcePath)
|
|
795
|
+
instance._extra_headers = extra_headers
|
|
796
|
+
return instance
|
|
797
|
+
|
|
762
798
|
# WebDAV servers known to be able to sign URLs. The values are lowercased
|
|
763
799
|
# server identifiers retrieved from the 'Server' header included in
|
|
764
800
|
# the response to a HTTP OPTIONS request.
|
|
@@ -805,39 +841,48 @@ class HttpResourcePath(ResourcePath):
|
|
|
805
841
|
# and is shared by all instances of this class.
|
|
806
842
|
_tcp_connector: TCPConnector | None = None
|
|
807
843
|
|
|
844
|
+
# Additional headers added to every request.
|
|
845
|
+
_extra_headers: dict[str, str] | None = None
|
|
846
|
+
|
|
808
847
|
@property
|
|
809
|
-
def metadata_session(self) ->
|
|
848
|
+
def metadata_session(self) -> _SessionWrapper:
|
|
810
849
|
"""Client session to send requests which do not require upload or
|
|
811
850
|
download of data, i.e. mostly metadata requests.
|
|
812
851
|
"""
|
|
852
|
+
session = None
|
|
813
853
|
if hasattr(self, "_metadata_session"):
|
|
814
854
|
if HttpResourcePath._pid == os.getpid():
|
|
815
|
-
|
|
855
|
+
session = self._metadata_session
|
|
816
856
|
else:
|
|
817
857
|
# The metadata session we have in cache was likely created by
|
|
818
858
|
# a parent process. Discard all the sessions in that store.
|
|
819
859
|
self._metadata_session_store.clear()
|
|
820
860
|
|
|
821
861
|
# Retrieve a new metadata session.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
862
|
+
if session is None:
|
|
863
|
+
HttpResourcePath._pid = os.getpid()
|
|
864
|
+
session = self._metadata_session_store.get(self)
|
|
865
|
+
self._metadata_session: requests.Session = session
|
|
866
|
+
return _SessionWrapper(session, extra_headers=self._extra_headers)
|
|
825
867
|
|
|
826
868
|
@property
|
|
827
|
-
def data_session(self) ->
|
|
869
|
+
def data_session(self) -> _SessionWrapper:
|
|
828
870
|
"""Client session for uploading and downloading data."""
|
|
871
|
+
session = None
|
|
829
872
|
if hasattr(self, "_data_session"):
|
|
830
873
|
if HttpResourcePath._pid == os.getpid():
|
|
831
|
-
|
|
874
|
+
session = self._data_session
|
|
832
875
|
else:
|
|
833
876
|
# The data session we have in cache was likely created by
|
|
834
877
|
# a parent process. Discard all the sessions in that store.
|
|
835
878
|
self._data_session_store.clear()
|
|
836
879
|
|
|
837
880
|
# Retrieve a new data session.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
881
|
+
if session is None:
|
|
882
|
+
HttpResourcePath._pid = os.getpid()
|
|
883
|
+
session = self._data_session_store.get(self)
|
|
884
|
+
self._data_session: requests.Session = session
|
|
885
|
+
return _SessionWrapper(session, extra_headers=self._extra_headers)
|
|
841
886
|
|
|
842
887
|
def _clear_sessions(self) -> None:
|
|
843
888
|
"""Close the socket connections that are still open.
|
|
@@ -1562,7 +1607,7 @@ class HttpResourcePath(ResourcePath):
|
|
|
1562
1607
|
url: str | None = None,
|
|
1563
1608
|
headers: dict[str, str] | None = None,
|
|
1564
1609
|
body: str | None = None,
|
|
1565
|
-
session:
|
|
1610
|
+
session: _SessionWrapper | None = None,
|
|
1566
1611
|
timeout: tuple[float, float] | None = None,
|
|
1567
1612
|
) -> requests.Response:
|
|
1568
1613
|
"""Send a webDAV request and correctly handle redirects.
|
|
@@ -1983,6 +2028,10 @@ class HttpResourcePath(ResourcePath):
|
|
|
1983
2028
|
with super()._openImpl(mode, encoding=encoding) as http_handle:
|
|
1984
2029
|
yield http_handle
|
|
1985
2030
|
|
|
2031
|
+
def _copy_extra_attributes(self, original_uri: ResourcePath) -> None:
|
|
2032
|
+
assert isinstance(original_uri, HttpResourcePath)
|
|
2033
|
+
self._extra_headers = original_uri._extra_headers
|
|
2034
|
+
|
|
1986
2035
|
|
|
1987
2036
|
def _dump_response(resp: requests.Response) -> None:
|
|
1988
2037
|
"""Log the contents of a HTTP or webDAV request and its response.
|
|
@@ -2193,3 +2242,95 @@ class DavProperty:
|
|
|
2193
2242
|
@property
|
|
2194
2243
|
def href(self) -> str:
|
|
2195
2244
|
return self._href
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
class _SessionWrapper(contextlib.AbstractContextManager):
|
|
2248
|
+
"""Wraps a `requests.Session` to allow header values to be injected with
|
|
2249
|
+
all requests.
|
|
2250
|
+
|
|
2251
|
+
Notes
|
|
2252
|
+
-----
|
|
2253
|
+
`requests.Session` already has a feature for setting headers globally, but
|
|
2254
|
+
our session objects are global and authorization headers can vary for each
|
|
2255
|
+
HttpResourcePath instance.
|
|
2256
|
+
"""
|
|
2257
|
+
|
|
2258
|
+
def __init__(self, session: requests.Session, *, extra_headers: dict[str, str] | None) -> None:
|
|
2259
|
+
self._session = session
|
|
2260
|
+
self._extra_headers = extra_headers
|
|
2261
|
+
|
|
2262
|
+
def __enter__(self) -> _SessionWrapper:
|
|
2263
|
+
self._session.__enter__()
|
|
2264
|
+
return self
|
|
2265
|
+
|
|
2266
|
+
def __exit__(
|
|
2267
|
+
self,
|
|
2268
|
+
exc_type: Any,
|
|
2269
|
+
exc_value: Any,
|
|
2270
|
+
traceback: Any,
|
|
2271
|
+
) -> None:
|
|
2272
|
+
return self._session.__exit__(exc_type, exc_value, traceback)
|
|
2273
|
+
|
|
2274
|
+
def get(
|
|
2275
|
+
self,
|
|
2276
|
+
url: str,
|
|
2277
|
+
*,
|
|
2278
|
+
timeout: tuple[float, float],
|
|
2279
|
+
allow_redirects: bool = True,
|
|
2280
|
+
stream: bool,
|
|
2281
|
+
headers: dict[str, str] | None = None,
|
|
2282
|
+
) -> requests.Response:
|
|
2283
|
+
return self._session.get(
|
|
2284
|
+
url,
|
|
2285
|
+
timeout=timeout,
|
|
2286
|
+
allow_redirects=allow_redirects,
|
|
2287
|
+
stream=stream,
|
|
2288
|
+
headers=self._augment_headers(headers),
|
|
2289
|
+
)
|
|
2290
|
+
|
|
2291
|
+
def head(
|
|
2292
|
+
self,
|
|
2293
|
+
url: str,
|
|
2294
|
+
*,
|
|
2295
|
+
timeout: tuple[float, float],
|
|
2296
|
+
allow_redirects: bool,
|
|
2297
|
+
stream: bool,
|
|
2298
|
+
headers: dict[str, str] | None = None,
|
|
2299
|
+
) -> requests.Response:
|
|
2300
|
+
return self._session.head(
|
|
2301
|
+
url,
|
|
2302
|
+
timeout=timeout,
|
|
2303
|
+
allow_redirects=allow_redirects,
|
|
2304
|
+
stream=stream,
|
|
2305
|
+
headers=self._augment_headers(headers),
|
|
2306
|
+
)
|
|
2307
|
+
|
|
2308
|
+
def request(
|
|
2309
|
+
self,
|
|
2310
|
+
method: str,
|
|
2311
|
+
url: str,
|
|
2312
|
+
*,
|
|
2313
|
+
data: str | bytes | BinaryIO | None,
|
|
2314
|
+
timeout: tuple[float, float],
|
|
2315
|
+
allow_redirects: bool,
|
|
2316
|
+
stream: bool,
|
|
2317
|
+
headers: dict[str, str] | None = None,
|
|
2318
|
+
) -> requests.Response:
|
|
2319
|
+
return self._session.request(
|
|
2320
|
+
method,
|
|
2321
|
+
url,
|
|
2322
|
+
data=data,
|
|
2323
|
+
timeout=timeout,
|
|
2324
|
+
allow_redirects=allow_redirects,
|
|
2325
|
+
stream=stream,
|
|
2326
|
+
headers=self._augment_headers(headers),
|
|
2327
|
+
)
|
|
2328
|
+
|
|
2329
|
+
def _augment_headers(self, headers: dict[str, str] | None) -> dict[str, str]:
|
|
2330
|
+
if headers is None:
|
|
2331
|
+
headers = {}
|
|
2332
|
+
|
|
2333
|
+
if self._extra_headers is not None:
|
|
2334
|
+
headers = headers | self._extra_headers
|
|
2335
|
+
|
|
2336
|
+
return headers
|
|
@@ -542,7 +542,7 @@ class S3ResourcePath(ResourcePath):
|
|
|
542
542
|
try:
|
|
543
543
|
self.client.copy_object(CopySource=copy_source, Bucket=self._bucket, Key=self.relativeToPathRoot)
|
|
544
544
|
except (self.client.exceptions.NoSuchKey, self.client.exceptions.NoSuchBucket) as err:
|
|
545
|
-
raise FileNotFoundError("No such resource to transfer: {self}") from err
|
|
545
|
+
raise FileNotFoundError(f"No such resource to transfer: {self}") from err
|
|
546
546
|
except ClientError as err:
|
|
547
547
|
translate_client_error(err, self)
|
|
548
548
|
raise
|
|
@@ -61,18 +61,18 @@ def _check_open(
|
|
|
61
61
|
"""
|
|
62
62
|
text_content = "abcdefghijklmnopqrstuvwxyz🙂"
|
|
63
63
|
bytes_content = uuid.uuid4().bytes
|
|
64
|
-
content_by_mode_suffix = {
|
|
64
|
+
content_by_mode_suffix: dict[str, str | bytes] = {
|
|
65
65
|
"": text_content,
|
|
66
66
|
"t": text_content,
|
|
67
67
|
"b": bytes_content,
|
|
68
68
|
}
|
|
69
|
-
empty_content_by_mode_suffix = {
|
|
69
|
+
empty_content_by_mode_suffix: dict[str, str | bytes] = {
|
|
70
70
|
"": "",
|
|
71
71
|
"t": "",
|
|
72
72
|
"b": b"",
|
|
73
73
|
}
|
|
74
74
|
# To appease mypy
|
|
75
|
-
double_content_by_mode_suffix = {
|
|
75
|
+
double_content_by_mode_suffix: dict[str, str | bytes] = {
|
|
76
76
|
"": text_content + text_content,
|
|
77
77
|
"t": text_content + text_content,
|
|
78
78
|
"b": bytes_content + bytes_content,
|
|
@@ -143,6 +143,16 @@ def _check_open(
|
|
|
143
143
|
content_read = read_buffer.read()
|
|
144
144
|
test_case.assertEqual(len(content_read), 0, f"Read: {content_read!r}, expected empty.")
|
|
145
145
|
|
|
146
|
+
# Write multiple chunks with flushing to ensure that any handles that
|
|
147
|
+
# cache without flushing work properly.
|
|
148
|
+
n = 3
|
|
149
|
+
with uri.open("w" + mode_suffix, **kwargs) as write_buffer:
|
|
150
|
+
for _ in range(n):
|
|
151
|
+
write_buffer.write(content)
|
|
152
|
+
write_buffer.flush()
|
|
153
|
+
with uri.open("r" + mode_suffix, **kwargs) as read_buffer:
|
|
154
|
+
test_case.assertEqual(read_buffer.read(), content * n)
|
|
155
|
+
|
|
146
156
|
# Write two copies of the content, overwriting the single copy there.
|
|
147
157
|
with uri.open("w" + mode_suffix, **kwargs) as write_buffer:
|
|
148
158
|
write_buffer.write(double_content)
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400/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.2400
|
|
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
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import hashlib
|
|
13
13
|
import io
|
|
14
14
|
import os.path
|
|
15
|
+
import pickle
|
|
15
16
|
import random
|
|
16
17
|
import shutil
|
|
17
18
|
import socket
|
|
@@ -20,6 +21,7 @@ import string
|
|
|
20
21
|
import tempfile
|
|
21
22
|
import time
|
|
22
23
|
import unittest
|
|
24
|
+
import unittest.mock
|
|
23
25
|
import warnings
|
|
24
26
|
from collections.abc import Callable
|
|
25
27
|
from threading import Thread
|
|
@@ -33,6 +35,7 @@ except ImportError:
|
|
|
33
35
|
|
|
34
36
|
import requests
|
|
35
37
|
import responses
|
|
38
|
+
import responses.matchers
|
|
36
39
|
|
|
37
40
|
import lsst.resources
|
|
38
41
|
from lsst.resources import ResourcePath
|
|
@@ -82,6 +85,38 @@ class GenericHttpTestCase(GenericTestCase, unittest.TestCase):
|
|
|
82
85
|
ResourcePath("http://user:password@server.com:3000/"),
|
|
83
86
|
)
|
|
84
87
|
|
|
88
|
+
@responses.activate
|
|
89
|
+
def test_extra_headers(self):
|
|
90
|
+
url = "http://test.example/something.txt"
|
|
91
|
+
path = HttpResourcePath.create_http_resource_path(
|
|
92
|
+
url, extra_headers={"Authorization": "Bearer my-token"}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.assertEqual(str(path), "http://test.example/something.txt")
|
|
96
|
+
self.assertEqual(path._extra_headers, {"Authorization": "Bearer my-token"})
|
|
97
|
+
|
|
98
|
+
# Make sure that headers are added to requests.
|
|
99
|
+
responses.add(
|
|
100
|
+
responses.GET,
|
|
101
|
+
url,
|
|
102
|
+
b"test",
|
|
103
|
+
match=[responses.matchers.header_matcher({"Authorization": "Bearer my-token"})],
|
|
104
|
+
)
|
|
105
|
+
self.assertEqual(path.read(), b"test")
|
|
106
|
+
|
|
107
|
+
# Extra headers should be preserved through pickle, to ensure that
|
|
108
|
+
# `mtransfer` and similar methods work in multi-process mode.
|
|
109
|
+
dump = pickle.dumps(path)
|
|
110
|
+
restored = pickle.loads(dump)
|
|
111
|
+
self.assertEqual(restored._extra_headers, {"Authorization": "Bearer my-token"})
|
|
112
|
+
|
|
113
|
+
# Extra headers should be preserved when making a modified copy of the
|
|
114
|
+
# ResourcePath using replace() or the ResourcePath constructor.
|
|
115
|
+
replacement = path.replace(forceDirectory=True)
|
|
116
|
+
self.assertEqual(replacement._extra_headers, {"Authorization": "Bearer my-token"})
|
|
117
|
+
copy = ResourcePath(path, forceDirectory=True)
|
|
118
|
+
self.assertEqual(copy._extra_headers, {"Authorization": "Bearer my-token"})
|
|
119
|
+
|
|
85
120
|
|
|
86
121
|
class HttpReadWriteWebdavTestCase(GenericReadWriteTestCase, unittest.TestCase):
|
|
87
122
|
"""Test with a real webDAV server, as opposed to mocking responses."""
|
|
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.2100 → lsst_resources-29.2025.2400}/doc/lsst.resources/internal-api.rst
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/davutils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/location.py
RENAMED
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/packageresource.py
RENAMED
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/s3utils.py
RENAMED
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/python/lsst/resources/schemeless.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_resources-29.2025.2100 → lsst_resources-29.2025.2400}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|