lsst-resources 29.2025.2100__py3-none-any.whl → 29.2025.2400__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.
@@ -79,7 +79,7 @@ class FileResourceHandle(BaseResourceHandle[U]):
79
79
  return self._fileHandle.fileno()
80
80
 
81
81
  def flush(self) -> None:
82
- self._fileHandle.close()
82
+ self._fileHandle.flush()
83
83
 
84
84
  @property
85
85
  def isatty(self) -> bool:
@@ -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
- (self.tell() - (self._last_flush_position or 0)) < s3_min_bits
172
- and self._closed != CloseStatus.CLOSING
173
- and not self._warned
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:
@@ -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
- return self.__class__(
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.
lsst/resources/http.py CHANGED
@@ -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) -> requests.Session:
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
- return self._metadata_session
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
- HttpResourcePath._pid = os.getpid()
823
- self._metadata_session: requests.Session = self._metadata_session_store.get(self)
824
- return self._metadata_session
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) -> requests.Session:
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
- return self._data_session
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
- HttpResourcePath._pid = os.getpid()
839
- self._data_session: requests.Session = self._data_session_store.get(self)
840
- return self._data_session
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: requests.Session | None = None,
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
lsst/resources/s3.py CHANGED
@@ -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
lsst/resources/tests.py CHANGED
@@ -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/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.2100"
2
+ __version__ = "29.2025.2400"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-resources
3
- Version: 29.2025.2100
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
@@ -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=neuZ-QNGIqwBavEq4xEBUnQveHeqACIuhVi4COhJI0A,73902
3
+ lsst/resources/_resourcePath.py,sha256=NLjOjz0ARFJxFwE9fw4HowFV0FrdYBX_jiyXODPFSVY,74273
4
4
  lsst/resources/dav.py,sha256=XQMAZgthvtJ4hUXZ0avBAHnAYjMYU3m2DPHDyQVT7a8,31547
5
5
  lsst/resources/davutils.py,sha256=xALuMRSvYroqn_Jz6bjnj43b4OgOgCJtNW49kyTtuiw,97983
6
6
  lsst/resources/file.py,sha256=-jPuoHvTEtx5tnDyNkfwhWAyX0cTwkuMd-JvJn9EGdE,23226
7
7
  lsst/resources/gs.py,sha256=Lpo5GAzH7R7HG8E5RMGOdP4j4hjWJn-k6M3OXj0nHQM,12783
8
- lsst/resources/http.py,sha256=Oa6YlTnHLd0ifkvBNhduI67oiRHE-iQupAL9TEbxyQo,87930
8
+ lsst/resources/http.py,sha256=gQNlmCyx9z_0GwrfBaOQ4WfV3Psz6BTfJYdTyolIntA,92441
9
9
  lsst/resources/location.py,sha256=x3Tq0x5o1OXYmZDxYBenUG1N71wtDhnjVAr3s2ZEiu8,7937
10
10
  lsst/resources/mem.py,sha256=VOWh7XxJPfqKcFdLZSjKEAfORQ2AHZHpxmjT8LniV60,1008
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=IPhNqKzseHsMS9RF9TAl8vhs-sFvXfdFsJCb7VMvuNg,29572
13
+ lsst/resources/s3.py,sha256=u2rZkg12C3JgIehIJOx69NeipdN6fawhvdZKffhdAF8,29573
14
14
  lsst/resources/s3utils.py,sha256=ojWf9BPrK9mhGQ8jvs4_8Nsqf9360e79U5FnPTxe24A,14576
15
15
  lsst/resources/schemeless.py,sha256=GfJcKzZ0XIeepfQdW4HPZWiZlSp_ej0SEtSiJTrDUQs,10666
16
- lsst/resources/tests.py,sha256=nTRvfAGrPnh2tsR8jzA-XPuWN1XECOW1f4dDveADmb8,44439
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=czd5myijXlfzN1l1OFP7x6hn5ASHoE02wXhI32IqLec,55
18
+ lsst/resources/version.py,sha256=AyivLkwA4FcYO6eiFsfTA_EuqMhydgvzBupNZlQZL-E,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
- lsst/resources/_resourceHandles/_fileResourceHandle.py,sha256=A7_WQPzD0ZlOzNmaI_TPdZybrNxrXPkNHWVla3UFxfs,3676
22
+ lsst/resources/_resourceHandles/_fileResourceHandle.py,sha256=2nC8tfP_ynAfjpzrtkw_1ahx1CuMEFpZ5mLmofSShUk,3676
23
23
  lsst/resources/_resourceHandles/_httpResourceHandle.py,sha256=Yami8IVGeru4bLQCag-OvGG0ltz1qyEg57FY4IEB87Y,10995
24
- lsst/resources/_resourceHandles/_s3ResourceHandle.py,sha256=NkDmPb9bm_zMvr6mMnb-tBmqJDt0yUJrt2gZXR8l7ok,12923
25
- lsst_resources-29.2025.2100.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
26
- lsst_resources-29.2025.2100.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
27
- lsst_resources-29.2025.2100.dist-info/METADATA,sha256=h1FqE1jzjXciOQ4whXXfG_UTucO0hZLDLboM-9IV-v8,2237
28
- lsst_resources-29.2025.2100.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
29
- lsst_resources-29.2025.2100.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
30
- lsst_resources-29.2025.2100.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
- lsst_resources-29.2025.2100.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5