lsst-resources 29.2025.1300__py3-none-any.whl → 29.2025.1500__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.
@@ -29,6 +29,15 @@ if TYPE_CHECKING:
29
29
  from ..http import HttpResourcePath
30
30
 
31
31
 
32
+ # Prevent circular import by copying this code. Can be removed as soon
33
+ # as separate dav implementation is implemented.
34
+ def _dav_to_http(url: str) -> str:
35
+ """Convert dav scheme in URL to http scheme."""
36
+ if url.startswith("dav"):
37
+ url = "http" + url.removeprefix("dav")
38
+ return url
39
+
40
+
32
41
  class HttpReadResourceHandle(BaseResourceHandle[bytes]):
33
42
  """HTTP-based specialization of `.BaseResourceHandle`.
34
43
 
@@ -159,7 +168,7 @@ class HttpReadResourceHandle(BaseResourceHandle[bytes]):
159
168
  # return the result
160
169
  self._completeBuffer = io.BytesIO()
161
170
  with time_this(self._log, msg="Read from remote resource %s", args=(self._url,)):
162
- resp = self._session.get(self._url, stream=False, timeout=self._timeout)
171
+ resp = self._session.get(_dav_to_http(self._url), stream=False, timeout=self._timeout)
163
172
  if (code := resp.status_code) not in (requests.codes.ok, requests.codes.partial):
164
173
  raise FileNotFoundError(f"Unable to read resource {self._url}; status code: {code}")
165
174
  self._completeBuffer.write(resp.content)
@@ -181,7 +190,9 @@ class HttpReadResourceHandle(BaseResourceHandle[bytes]):
181
190
  with time_this(
182
191
  self._log, msg="Read from remote resource %s using headers %s", args=(self._url, headers)
183
192
  ):
184
- resp = self._session.get(self._url, stream=False, timeout=self._timeout, headers=headers)
193
+ resp = self._session.get(
194
+ _dav_to_http(self._url), stream=False, timeout=self._timeout, headers=headers
195
+ )
185
196
 
186
197
  if resp.status_code == requests.codes.range_not_satisfiable:
187
198
  # Must have run off the end of the file. A standard file handle
@@ -23,8 +23,6 @@ import multiprocessing
23
23
  import os
24
24
  import posixpath
25
25
  import re
26
- import shutil
27
- import tempfile
28
26
  import urllib.parse
29
27
  from functools import cache
30
28
  from pathlib import Path, PurePath, PurePosixPath
@@ -41,7 +39,7 @@ from collections.abc import Iterable, Iterator
41
39
  from typing import TYPE_CHECKING, Any, Literal, NamedTuple, overload
42
40
 
43
41
  from ._resourceHandles._baseResourceHandle import ResourceHandleProtocol
44
- from .utils import ensure_directory_is_writeable
42
+ from .utils import get_tempdir
45
43
 
46
44
  if TYPE_CHECKING:
47
45
  from .utils import TransactionProtocol
@@ -336,6 +334,10 @@ class ResourcePath: # numpydoc ignore=PR02
336
334
  elif parsed.scheme.startswith("http"):
337
335
  from .http import HttpResourcePath
338
336
 
337
+ subclass = HttpResourcePath
338
+ elif parsed.scheme in {"dav", "davs"}:
339
+ from .http import HttpResourcePath
340
+
339
341
  subclass = HttpResourcePath
340
342
  elif parsed.scheme == "gs":
341
343
  from .gs import GSResourcePath
@@ -1136,35 +1138,23 @@ class ResourcePath: # numpydoc ignore=PR02
1136
1138
  prefix : `ResourcePath`, optional
1137
1139
  Temporary directory to use (can be any scheme). Without this the
1138
1140
  path will be formed as a local file URI in a temporary directory
1139
- created by `tempfile.mkdtemp`. Ensuring that the prefix
1140
- location exists is the responsibility of the caller.
1141
+ obtained from `lsst.resources.utils.get_tempdir`. Ensuring that the
1142
+ prefix location exists is the responsibility of the caller.
1141
1143
  suffix : `str`, optional
1142
1144
  A file suffix to be used. The ``.`` should be included in this
1143
1145
  suffix.
1144
1146
  delete : `bool`, optional
1145
1147
  By default the resource will be deleted when the context manager
1146
1148
  is exited. Setting this flag to `False` will leave the resource
1147
- alone. `False` will also retain any directories that may have
1148
- been created.
1149
+ alone.
1149
1150
 
1150
1151
  Yields
1151
1152
  ------
1152
1153
  uri : `ResourcePath`
1153
1154
  The temporary URI. Will be removed when the context is completed.
1154
1155
  """
1155
- use_tempdir = False
1156
1156
  if prefix is None:
1157
- directory = tempfile.mkdtemp()
1158
- # If the user has set a umask that restricts the owner-write bit,
1159
- # the directory returned from mkdtemp may not initially be
1160
- # writeable by us
1161
- ensure_directory_is_writeable(directory)
1162
-
1163
- prefix = ResourcePath(directory, forceDirectory=True, isTemporary=True)
1164
- # Record that we need to delete this directory. Can not rely
1165
- # on isTemporary flag since an external prefix may have that
1166
- # set as well.
1167
- use_tempdir = True
1157
+ prefix = ResourcePath(get_tempdir(), forceDirectory=True)
1168
1158
 
1169
1159
  # Need to create a randomized file name. For consistency do not
1170
1160
  # use mkstemp for local and something else for remote. Additionally
@@ -1183,13 +1173,10 @@ class ResourcePath: # numpydoc ignore=PR02
1183
1173
  yield temporary_uri
1184
1174
  finally:
1185
1175
  if delete:
1186
- if use_tempdir:
1187
- shutil.rmtree(prefix.ospath, ignore_errors=True)
1188
- else:
1189
- with contextlib.suppress(FileNotFoundError):
1190
- # It's okay if this does not work because the user
1191
- # removed the file.
1192
- temporary_uri.remove()
1176
+ with contextlib.suppress(FileNotFoundError):
1177
+ # It's okay if this does not work because the user
1178
+ # removed the file.
1179
+ temporary_uri.remove()
1193
1180
 
1194
1181
  def read(self, size: int = -1) -> bytes:
1195
1182
  """Open the resource and return the contents in bytes.
lsst/resources/http.py CHANGED
@@ -26,7 +26,6 @@ import random
26
26
  import re
27
27
  import ssl
28
28
  import stat
29
- import tempfile
30
29
  from collections.abc import Iterator
31
30
  from typing import TYPE_CHECKING, Any, BinaryIO, cast
32
31
 
@@ -60,6 +59,7 @@ from lsst.utils.timer import time_this
60
59
  from ._resourceHandles import ResourceHandleProtocol
61
60
  from ._resourceHandles._httpResourceHandle import HttpReadResourceHandle, parse_content_range_header
62
61
  from ._resourcePath import ResourcePath
62
+ from .utils import get_tempdir
63
63
 
64
64
  if TYPE_CHECKING:
65
65
  from .utils import TransactionProtocol
@@ -112,6 +112,13 @@ def _calc_tmpdir_buffer_size(tmpdir: str) -> int:
112
112
  return max(10 * fsstats.f_bsize, 256 * 4096)
113
113
 
114
114
 
115
+ def _dav_to_http(url: str) -> str:
116
+ """Convert dav scheme in URL to http scheme."""
117
+ if url.startswith("dav"):
118
+ url = "http" + url.removeprefix("dav")
119
+ return url
120
+
121
+
115
122
  class HttpResourcePathConfig:
116
123
  """Configuration class to encapsulate the configurable items used by class
117
124
  HttpResourcePath.
@@ -377,18 +384,7 @@ class HttpResourcePathConfig:
377
384
  if self._tmpdir_buffersize is not None:
378
385
  return self._tmpdir_buffersize
379
386
 
380
- # Use the value of environment variables 'LSST_RESOURCES_TMPDIR' or
381
- # 'TMPDIR', if defined. Otherwise use the system temporary directory,
382
- # with a last-resort fallback to the current working directory if
383
- # nothing else is available.
384
- tmpdir = None
385
- for dir in (os.getenv(v) for v in ("LSST_RESOURCES_TMPDIR", "TMPDIR")):
386
- if dir and os.path.isdir(dir):
387
- tmpdir = dir
388
- break
389
-
390
- if tmpdir is None:
391
- tmpdir = tempfile.gettempdir()
387
+ tmpdir = get_tempdir()
392
388
 
393
389
  # Compute the block size as 256 blocks of typical size
394
390
  # (i.e. 4096 bytes) or 10 times the file system block size,
@@ -441,7 +437,7 @@ def _get_dav_and_server_headers(path: ResourcePath | str) -> tuple[str | None, s
441
437
  config = HttpResourcePathConfig()
442
438
  with SessionStore(config=config).get(path) as session:
443
439
  resp = session.options(
444
- str(path),
440
+ _dav_to_http(str(path)),
445
441
  stream=False,
446
442
  timeout=config.timeout,
447
443
  )
@@ -601,7 +597,7 @@ class SessionStore:
601
597
  def _make_session(self, rpath: ResourcePath) -> requests.Session:
602
598
  """Make a new session configured from values from the environment."""
603
599
  session = requests.Session()
604
- root_uri = str(rpath.root_uri())
600
+ root_uri = _dav_to_http(str(rpath.root_uri()))
605
601
  log.debug("Creating new HTTP session for endpoint %s ...", root_uri)
606
602
  retries = Retry(
607
603
  # Total number of retries to allow. Takes precedence over other
@@ -661,8 +657,9 @@ class SessionStore:
661
657
  # from request to request. Systematically persisting connections to
662
658
  # those servers may exhaust their capabilities when there are thousands
663
659
  # of simultaneous clients.
660
+ scheme = _dav_to_http(rpath.scheme)
664
661
  session.mount(
665
- f"{rpath.scheme}://",
662
+ f"{scheme}://",
666
663
  HTTPAdapter(
667
664
  pool_connections=self._num_pools,
668
665
  pool_maxsize=0,
@@ -1130,7 +1127,7 @@ class HttpResourcePath(ResourcePath):
1130
1127
  stream = size > 0
1131
1128
  with self.data_session as session:
1132
1129
  with time_this(log, msg="GET %s", args=(self,)):
1133
- resp = session.get(self.geturl(), stream=stream, timeout=self._config.timeout)
1130
+ resp = session.get(_dav_to_http(self.geturl()), stream=stream, timeout=self._config.timeout)
1134
1131
 
1135
1132
  if resp.status_code != requests.codes.ok: # 200
1136
1133
  raise FileNotFoundError(
@@ -1351,6 +1348,9 @@ class HttpResourcePath(ResourcePath):
1351
1348
  or not self.is_webdav_endpoint
1352
1349
  or self.server not in HttpResourcePath.SUPPORTED_URL_SIGNERS
1353
1350
  ):
1351
+ if self.scheme.startswith("dav") and fsspec:
1352
+ # Not webdav so convert to http.
1353
+ return fsspec.url_to_fs(_dav_to_http(self.geturl()))
1354
1354
  return super().to_fsspec()
1355
1355
 
1356
1356
  if self.isdir():
@@ -1520,7 +1520,7 @@ class HttpResourcePath(ResourcePath):
1520
1520
  # to both the front end and back end servers are closed after the
1521
1521
  # download operation is finished.
1522
1522
  with self.data_session as session:
1523
- resp = session.get(self.geturl(), stream=True, timeout=self._config.timeout)
1523
+ resp = session.get(_dav_to_http(self.geturl()), stream=True, timeout=self._config.timeout)
1524
1524
  if resp.status_code != requests.codes.ok:
1525
1525
  raise FileNotFoundError(
1526
1526
  f"Unable to download resource {self}; status: {resp.status_code} {resp.reason}"
@@ -1633,7 +1633,7 @@ class HttpResourcePath(ResourcePath):
1633
1633
  for _ in range(max_redirects := 5):
1634
1634
  resp = session.request(
1635
1635
  method,
1636
- url,
1636
+ _dav_to_http(url),
1637
1637
  data=body,
1638
1638
  headers=headers,
1639
1639
  stream=False,
@@ -1791,7 +1791,7 @@ class HttpResourcePath(ResourcePath):
1791
1791
  src : `HttpResourcePath`
1792
1792
  The source of the contents to move to `self`.
1793
1793
  """
1794
- headers = {"Destination": self.geturl()}
1794
+ headers = {"Destination": _dav_to_http(self.geturl())}
1795
1795
  resp = self._send_webdav_request(method, url=src.geturl(), headers=headers, session=self.data_session)
1796
1796
  if resp.status_code in (requests.codes.created, requests.codes.no_content):
1797
1797
  return
@@ -1898,7 +1898,7 @@ class HttpResourcePath(ResourcePath):
1898
1898
  if self._config.send_expect_on_put or self.server == "dcache":
1899
1899
  headers["Expect"] = "100-continue"
1900
1900
 
1901
- url = self.geturl()
1901
+ url = _dav_to_http(self.geturl())
1902
1902
 
1903
1903
  # Use the session as a context manager to ensure the underlying
1904
1904
  # connections are closed after finishing uploading the data.
lsst/resources/utils.py CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
- __all__ = ("NoTransaction", "TransactionProtocol", "os2posix", "posix2os")
14
+ __all__ = ("NoTransaction", "TransactionProtocol", "get_tempdir", "os2posix", "posix2os")
15
15
 
16
16
  import contextlib
17
17
  import logging
@@ -21,6 +21,7 @@ import shutil
21
21
  import stat
22
22
  import tempfile
23
23
  from collections.abc import Callable, Iterator
24
+ from functools import cache
24
25
  from pathlib import Path, PurePath, PurePosixPath
25
26
  from typing import Any, Protocol
26
27
 
@@ -93,6 +94,35 @@ def posix2os(posix: PurePath | str) -> str:
93
94
  return os.path.join(*paths)
94
95
 
95
96
 
97
+ @cache
98
+ def get_tempdir() -> str:
99
+ """Get POSIX path to temporary directory.
100
+
101
+ Returns
102
+ -------
103
+ tmpdir : `str`
104
+ Path to the default temporary directory location.
105
+
106
+ Notes
107
+ -----
108
+ Uses the value of environment variables ``LSST_RESOURCES_TMPDIR`` or
109
+ ``TMPDIR``, if defined. Otherwise use the system temporary directory,
110
+ with a last-resort fallback to the current working directory if
111
+ nothing else is available.
112
+ """
113
+ tmpdir = None
114
+ # $TMPDIR is also checked with getttempdir() below.
115
+ for dir in (os.getenv(v) for v in ("LSST_RESOURCES_TMPDIR", "TMPDIR")):
116
+ if dir and os.path.isdir(dir):
117
+ tmpdir = dir
118
+ break
119
+
120
+ if tmpdir is None:
121
+ tmpdir = tempfile.gettempdir()
122
+
123
+ return tmpdir
124
+
125
+
96
126
  class NoTransaction:
97
127
  """A simple emulation of the
98
128
  `~lsst.daf.butler.core.datastore.DatastoreTransaction` class.
lsst/resources/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.1300"
2
+ __version__ = "29.2025.1500"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-resources
3
- Version: 29.2025.1300
3
+ Version: 29.2025.1500
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,9 +1,9 @@
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=cx7Cxcgt9fK5mvo1IX2KSSaiSP5ifzocxgknCnlKggI,65550
3
+ lsst/resources/_resourcePath.py,sha256=xTVyDHD-UHlF5FeDvSXXnsmOuoSFnORZD_wMksxiFfA,64926
4
4
  lsst/resources/file.py,sha256=-jPuoHvTEtx5tnDyNkfwhWAyX0cTwkuMd-JvJn9EGdE,23226
5
5
  lsst/resources/gs.py,sha256=Lpo5GAzH7R7HG8E5RMGOdP4j4hjWJn-k6M3OXj0nHQM,12783
6
- lsst/resources/http.py,sha256=U4gv-IZsqNJ7NmAhVlOIY84slqpuHnl1bWgP6h6MssU,88221
6
+ lsst/resources/http.py,sha256=JW3cBe4MERyjopFKkELui1BRr4b4Mkgp0Iqt9YFIxuc,88227
7
7
  lsst/resources/location.py,sha256=x3Tq0x5o1OXYmZDxYBenUG1N71wtDhnjVAr3s2ZEiu8,7937
8
8
  lsst/resources/mem.py,sha256=VOWh7XxJPfqKcFdLZSjKEAfORQ2AHZHpxmjT8LniV60,1008
9
9
  lsst/resources/packageresource.py,sha256=vnfeRlpVwpC5cDQZE6Lnh8EH6oZy1sH2vLz9ONYjJ4k,6817
@@ -12,17 +12,17 @@ lsst/resources/s3.py,sha256=wrOMdFWltxpGEWeL--kPCbk5Le_viCIsEn4lOPZbXhM,24124
12
12
  lsst/resources/s3utils.py,sha256=cKJ9GWHETHhn1djezyikWwAaw4k1B3hFvfif96THHDQ,14355
13
13
  lsst/resources/schemeless.py,sha256=GfJcKzZ0XIeepfQdW4HPZWiZlSp_ej0SEtSiJTrDUQs,10666
14
14
  lsst/resources/tests.py,sha256=MLB8hERKuNredzzg3Qq9M_U7IesV3xrbcjFwKuMp3Ok,43513
15
- lsst/resources/utils.py,sha256=D2J0d0n5-jotQ3AWFotXTi2PFN0S9C6HK41G1uO-Y3Q,5891
16
- lsst/resources/version.py,sha256=tryL-NT2fHNIinBXQOOXxLFsVaDOMR53wZgDt_5tYb0,55
15
+ lsst/resources/utils.py,sha256=IHVrOdj0szNWxiXk-jbZu1RhTR8WXks1vI9JCpBxeBA,6706
16
+ lsst/resources/version.py,sha256=altncYankWkwG1F2OVHhPl0np72lUhdVRAOSqBtd-kc,55
17
17
  lsst/resources/_resourceHandles/__init__.py,sha256=zOcZ8gVEBdAWcHJaZabA8Vdq-wAVcxjbmA_1b1IWM6M,76
18
18
  lsst/resources/_resourceHandles/_baseResourceHandle.py,sha256=lQwxDOmFUNJndTxsjpz-HxrQBL0L-z4aXQocHdOEI7c,4676
19
19
  lsst/resources/_resourceHandles/_fileResourceHandle.py,sha256=A7_WQPzD0ZlOzNmaI_TPdZybrNxrXPkNHWVla3UFxfs,3676
20
- lsst/resources/_resourceHandles/_httpResourceHandle.py,sha256=w14dnwez4E1Kjo2Yxr0DsUUAja9cyBfkHzqI4WdKW3o,10541
20
+ lsst/resources/_resourceHandles/_httpResourceHandle.py,sha256=JRjpE-ZQfgKX5OyVLulIbzW38FdhovcoOd1D4rhb5vk,10900
21
21
  lsst/resources/_resourceHandles/_s3ResourceHandle.py,sha256=NkDmPb9bm_zMvr6mMnb-tBmqJDt0yUJrt2gZXR8l7ok,12923
22
- lsst_resources-29.2025.1300.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
23
- lsst_resources-29.2025.1300.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
24
- lsst_resources-29.2025.1300.dist-info/METADATA,sha256=AMSEcW0mFiy-IXMseM2_Cpp9xgThvJnDtLbbG32WNjg,2237
25
- lsst_resources-29.2025.1300.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
26
- lsst_resources-29.2025.1300.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
27
- lsst_resources-29.2025.1300.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
- lsst_resources-29.2025.1300.dist-info/RECORD,,
22
+ lsst_resources-29.2025.1500.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
23
+ lsst_resources-29.2025.1500.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
24
+ lsst_resources-29.2025.1500.dist-info/METADATA,sha256=8tmxas_i8S1F0lwXkFr7MwlERU1h1TArUW-xUqp4FwE,2237
25
+ lsst_resources-29.2025.1500.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
26
+ lsst_resources-29.2025.1500.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
27
+ lsst_resources-29.2025.1500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
+ lsst_resources-29.2025.1500.dist-info/RECORD,,