lsst-resources 29.2025.4800__py3-none-any.whl → 30.0.0rc3__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.
@@ -386,6 +386,11 @@ class ResourcePath: # numpydoc ignore=PR02
386
386
  from .mem import InMemoryResourcePath
387
387
 
388
388
  subclass = InMemoryResourcePath
389
+ elif parsed.scheme == "eups":
390
+ # EUPS package root.
391
+ from .eups import EupsResourcePath
392
+
393
+ subclass = EupsResourcePath
389
394
  else:
390
395
  raise NotImplementedError(
391
396
  f"No URI support for scheme: '{parsed.scheme}' in {parsed.geturl()}"
@@ -396,11 +401,15 @@ class ResourcePath: # numpydoc ignore=PR02
396
401
  )
397
402
 
398
403
  # It is possible for the class to change from schemeless
399
- # to file so handle that
404
+ # to file or eups so handle that
400
405
  if parsed.scheme == "file":
401
406
  from .file import FileResourcePath
402
407
 
403
408
  subclass = FileResourcePath
409
+ elif parsed.scheme == "eups":
410
+ from .eups import EupsResourcePath
411
+
412
+ subclass = EupsResourcePath
404
413
 
405
414
  # Now create an instance of the correct subclass and set the
406
415
  # attributes directly
@@ -410,8 +419,13 @@ class ResourcePath: # numpydoc ignore=PR02
410
419
  if isTemporary is None:
411
420
  isTemporary = False
412
421
  self.isTemporary = isTemporary
422
+ self._set_proxy()
413
423
  return self
414
424
 
425
+ def _set_proxy(self) -> None:
426
+ """Calculate internal proxy for externally visible resource path."""
427
+ pass
428
+
415
429
  @property
416
430
  def scheme(self) -> str:
417
431
  """Return the URI scheme.
lsst/resources/eups.py ADDED
@@ -0,0 +1,101 @@
1
+ # This file is part of lsst-resources.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (https://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # Use of this source code is governed by a 3-clause BSD-style
10
+ # license that can be found in the LICENSE file.
11
+
12
+ from __future__ import annotations
13
+
14
+ __all__ = ("EupsResourcePath",)
15
+
16
+ import logging
17
+ import posixpath
18
+ import urllib.parse
19
+
20
+ from lsst.utils import getPackageDir
21
+
22
+ from ._resourcePath import ResourcePath
23
+ from .proxied import ProxiedResourcePath
24
+ from .utils import os2posix
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ class EupsResourcePath(ProxiedResourcePath):
30
+ """URI referring to an EUPS package.
31
+
32
+ These URIs look like: ``eups://daf_butler/configs/file.yaml``
33
+ where the network location is the EUPS package name.
34
+
35
+ Internally they are proxied by either ``file`` URIs or ``resource`` URIs.
36
+ If an ``{product}_DIR`` environment variable is found it will be used
37
+ and internally a ``file`` URI will be created. If no environment variable
38
+ is found an attempt will be made to convert the EUPS product name to
39
+ a python package and a ``resource`` URI will be returned with
40
+ ``/resources`` appended. The convention is that any package supporting
41
+ an EUPS URI outside an EUPS environment will also have made available
42
+ the support files as package resources.
43
+
44
+ If it is known that the package supports package resources it is always
45
+ better to use that URI form explicitly since it is more robust since
46
+ not all EUPS packages can reliably be converted to python packages
47
+ without EUPS.
48
+ """
49
+
50
+ quotePaths = False
51
+ _proxy: ResourcePath | None = None
52
+ _default_namespace: str = "lsst"
53
+
54
+ def _set_proxy(self) -> None:
55
+ """Calculate the internal `ResourcePath` corresponding to the public
56
+ version.
57
+ """
58
+ # getPackageDir returns an absolute path.
59
+ try:
60
+ eups_path = getPackageDir(self.netloc)
61
+ log.debug("Found EUPS package %s via env var", self.netloc)
62
+ except LookupError:
63
+ eups_path = ""
64
+ if eups_path:
65
+ # Must convert this path into a file URI.
66
+ new_path = posixpath.join(os2posix(eups_path), os2posix(self.path.lstrip("/")))
67
+ parsed = self._uri._replace(path=urllib.parse.quote(new_path), scheme="file", netloc="")
68
+ self._proxy = ResourcePath(parsed)
69
+ return
70
+
71
+ # If there is no _DIR env var we need to look for python package
72
+ # resource. There is no guaranteed way to generated a python package
73
+ # from an EUPS product name.
74
+ # daf_butler -> lsst.daf.butler
75
+ # image_cutout_backend -> lsst.image_cutout_backend
76
+ # astro_metadata_translator -> astro_metadata_translator
77
+ product = self.netloc
78
+ variants = (
79
+ product,
80
+ self._default_namespace + "." + product.replace("_", "."),
81
+ self._default_namespace + "." + product,
82
+ product.replace("_", "."),
83
+ )
84
+ for variant in variants:
85
+ proxy = ResourcePath(f"resource://{variant}/resources", forceDirectory=True)
86
+ # This can be slow because package imports happen but there is
87
+ # no other way to check that we have the correct variant.
88
+ log.debug("Trying variant %s", proxy)
89
+ if proxy.exists():
90
+ self._proxy = proxy
91
+ if self.path:
92
+ self._proxy = self._proxy.join(self.path.lstrip("/"))
93
+ log.debug(f"Found variant {variant}")
94
+ return
95
+
96
+ # Can not find an actively set up package or resources for this EUPS
97
+ # product. Return without setting a proxy to allow standard URI
98
+ # path manipulations to happen but defer failure until someone tries
99
+ # to use the proxy for read.
100
+ log.debug("Could not find any files corresponding to %s", self)
101
+ return
@@ -42,6 +42,8 @@ class PackageResourcePath(ResourcePath):
42
42
  resource name.
43
43
  """
44
44
 
45
+ quotePaths = False
46
+
45
47
  def _get_ref(self) -> resources.abc.Traversable | None:
46
48
  """Obtain the object representing the resource.
47
49
 
@@ -0,0 +1,148 @@
1
+ # This file is part of lsst-resources.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (https://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # Use of this source code is governed by a 3-clause BSD-style
10
+ # license that can be found in the LICENSE file.
11
+
12
+ from __future__ import annotations
13
+
14
+ __all__ = ("ProxiedResourcePath",)
15
+
16
+ import contextlib
17
+ import logging
18
+ import re
19
+ from abc import ABC, abstractmethod
20
+ from collections.abc import Iterator
21
+
22
+ from ._resourcePath import ResourceHandleProtocol, ResourcePath, ResourcePathExpression
23
+ from .utils import TransactionProtocol
24
+
25
+ try:
26
+ import fsspec
27
+ from fsspec.spec import AbstractFileSystem
28
+ except ImportError:
29
+ fsspec = None
30
+ AbstractFileSystem = type
31
+
32
+
33
+ log = logging.getLogger(__name__)
34
+
35
+
36
+ class ProxiedResourcePath(ABC, ResourcePath):
37
+ """URI that is represented internally by another type of URI for file I/O.
38
+
39
+ For example ``abc://xyz/file.txt`` could be the public URI form but
40
+ internally all file access is forwarded to a ``file`` URI.
41
+ """
42
+
43
+ _proxy: ResourcePath | None = None
44
+
45
+ @abstractmethod
46
+ def _set_proxy(self) -> None:
47
+ """Calculate the internal `ResourcePath` corresponding to the public
48
+ version.
49
+ """
50
+ raise NotImplementedError("Proxy must be configured")
51
+
52
+ def _get_proxy(self) -> ResourcePath:
53
+ """Retrieve the proxied ResourcePath."""
54
+ proxy = self._proxy
55
+ if proxy is None:
56
+ raise FileNotFoundError(f"Internal error: No proxy ResourcePath available for {self}")
57
+ return proxy
58
+
59
+ def to_fsspec(self) -> tuple[AbstractFileSystem, str]:
60
+ try:
61
+ proxy = self._get_proxy()
62
+ except FileNotFoundError:
63
+ raise NotImplementedError(f"No proxy registered for {self}. Resource does not exist.") from None
64
+ return proxy.to_fsspec()
65
+
66
+ def isdir(self) -> bool:
67
+ if self.dirLike is None:
68
+ try:
69
+ proxy = self._get_proxy()
70
+ except FileNotFoundError:
71
+ return False
72
+ self.dirLike = proxy.isdir()
73
+ return self.dirLike
74
+
75
+ def exists(self) -> bool:
76
+ try:
77
+ proxy = self._get_proxy()
78
+ except FileNotFoundError:
79
+ # If there is no proxy registered then the resource can not exist.
80
+ return False
81
+ return proxy.exists()
82
+
83
+ def remove(self) -> None:
84
+ proxy = self._get_proxy()
85
+ proxy.remove()
86
+
87
+ def read(self, size: int = -1) -> bytes:
88
+ proxy = self._get_proxy()
89
+ return proxy.read(size=size)
90
+
91
+ @contextlib.contextmanager
92
+ def as_local(
93
+ self, multithreaded: bool = True, tmpdir: ResourcePathExpression | None = None
94
+ ) -> Iterator[ResourcePath]:
95
+ proxy = self._get_proxy()
96
+ with proxy.as_local(multithreaded=multithreaded, tmpdir=tmpdir) as loc:
97
+ yield loc
98
+
99
+ @contextlib.contextmanager
100
+ def open(
101
+ self,
102
+ mode: str = "r",
103
+ *,
104
+ encoding: str | None = None,
105
+ prefer_file_temporary: bool = False,
106
+ ) -> Iterator[ResourceHandleProtocol]:
107
+ proxy = self._get_proxy()
108
+ with proxy.open(mode, encoding=encoding, prefer_file_temporary=prefer_file_temporary) as fh:
109
+ yield fh
110
+
111
+ def walk(
112
+ self, file_filter: str | re.Pattern | None = None
113
+ ) -> Iterator[list | tuple[ResourcePath, list[str], list[str]]]:
114
+ try:
115
+ proxy = self._get_proxy()
116
+ except FileNotFoundError as e:
117
+ raise ValueError(str(e)) from None
118
+ for proxied_root, dirs, files in proxy.walk(file_filter=file_filter):
119
+ # Need to return the directory in the original form and not the
120
+ # proxy form.
121
+ relative_to_self = proxied_root.path.removeprefix(proxy.path)
122
+ root = self.replace(path=self._pathModule.join(self.path, relative_to_self))
123
+ yield root, dirs, files
124
+
125
+ def size(self) -> int:
126
+ proxy = self._get_proxy()
127
+ return proxy.size()
128
+
129
+ def write(self, data: bytes, overwrite: bool = True) -> None:
130
+ proxy = self._get_proxy()
131
+ proxy.write(data, overwrite=overwrite)
132
+
133
+ def mkdir(self) -> None:
134
+ proxy = self._get_proxy()
135
+ proxy.mkdir()
136
+
137
+ def transfer_from(
138
+ self,
139
+ src: ResourcePath,
140
+ transfer: str = "copy",
141
+ overwrite: bool = False,
142
+ transaction: TransactionProtocol | None = None,
143
+ multithreaded: bool = True,
144
+ ) -> None:
145
+ proxy = self._get_proxy()
146
+ proxy.transfer_from(
147
+ src, transfer=transfer, overwrite=overwrite, transaction=transaction, multithreaded=multithreaded
148
+ )
@@ -16,6 +16,7 @@ __all__ = ("SchemelessResourcePath",)
16
16
  import logging
17
17
  import os
18
18
  import os.path
19
+ import re
19
20
  import stat
20
21
  import urllib.parse
21
22
  from pathlib import PurePath
@@ -211,12 +212,23 @@ class SchemelessResourcePath(FileResourcePath):
211
212
  expandedPath = os.path.expanduser(urllib.parse.unquote(parsed.path))
212
213
 
213
214
  # We might also be receiving a path containing environment variables
214
- # so expand those here
215
+ # so expand those here, although we treat $X_DIR at the start of the
216
+ # path as a special EUPS URI. This allows us to handle EUPS-style
217
+ # env var specifications even if EUPS has not set them.
218
+ # Support $X_DIR and ${X_DIR} variants at the start of the path.
219
+ if eups := re.match(r"(\$\{?([A-Z_]+)_DIR\}?)/", expandedPath):
220
+ replacements["scheme"] = "eups"
221
+ # Two matching groups: the entire env var, and the EUPS product.
222
+ replacements["netloc"] = eups.group(2).lower()
223
+ expandedPath = expandedPath.removeprefix(eups.group(1))
224
+
215
225
  expandedPath = os.path.expandvars(expandedPath)
216
226
 
217
- # Ensure that this becomes a file URI if it is already absolute
227
+ # Ensure that this becomes a file URI if it is already absolute, unless
228
+ # we already overrode it above.
218
229
  if os.path.isabs(expandedPath):
219
- replacements["scheme"] = "file"
230
+ if "scheme" not in replacements:
231
+ replacements["scheme"] = "file"
220
232
  # Keep in OS form for now to simplify later logic
221
233
  replacements["path"] = os.path.normpath(expandedPath)
222
234
  elif forceAbsolute:
@@ -265,9 +277,9 @@ class SchemelessResourcePath(FileResourcePath):
265
277
  if not replacements["path"].endswith(os.sep):
266
278
  replacements["path"] += os.sep
267
279
 
268
- if "scheme" in replacements:
280
+ if "scheme" in replacements and replacements["scheme"] == "file":
269
281
  # This is now meant to be a URI path so force to posix
270
- # and quote
282
+ # and quote. EUPS URIs are not quoted.
271
283
  replacements["path"] = urllib.parse.quote(os2posix(replacements["path"]))
272
284
 
273
285
  # ParseResult is a NamedTuple so _replace is standard API
lsst/resources/tests.py CHANGED
@@ -417,11 +417,19 @@ class GenericTestCase(_GenericTestCase):
417
417
  def test_escapes(self) -> None:
418
418
  """Special characters in file paths."""
419
419
  src = self.root_uri.join("bbb/???/test.txt")
420
- self.assertNotIn("???", src.path)
420
+ quotes = src.quotePaths
421
+
422
+ if quotes:
423
+ self.assertNotIn("???", src.path)
424
+ else:
425
+ self.assertIn("???", src.path)
421
426
  self.assertIn("???", src.unquoted_path)
422
427
 
423
428
  file = src.updatedFile("tests??.txt")
424
- self.assertNotIn("??.txt", file.path)
429
+ if quotes:
430
+ self.assertNotIn("??.txt", file.path)
431
+ else:
432
+ self.assertIn("??.txt", file.path)
425
433
 
426
434
  src = src.updatedFile("tests??.txt")
427
435
  self.assertIn("??.txt", src.unquoted_path)
@@ -456,7 +464,10 @@ class GenericTestCase(_GenericTestCase):
456
464
 
457
465
  fnew2 = fdir.join(new2name)
458
466
  self.assertTrue(fnew2.unquoted_path.endswith(new2name))
459
- self.assertNotIn("###", fnew2.path)
467
+ if quotes:
468
+ self.assertNotIn("###", fnew2.path)
469
+ else:
470
+ self.assertIn("###", fnew2.path)
460
471
 
461
472
  # Test that children relative to schemeless and file schemes
462
473
  # still return the same unquoted name
lsst/resources/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "29.2025.4800"
2
+ __version__ = "30.0.0rc3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-resources
3
- Version: 29.2025.4800
3
+ Version: 30.0.0rc3
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-Expression: BSD-3-Clause
@@ -1,31 +1,33 @@
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=fnB8XNUfk25lV378U0kg1O6c1QnSJL7F9iqaEt08IOA,74508
3
+ lsst/resources/_resourcePath.py,sha256=VLW9YDADwnvK2WOypmWPCstd6iTnvDns1Jtf6UAR03E,74980
4
4
  lsst/resources/dav.py,sha256=ZYP7PnQzS7epm5woxnn1_t1XhsPQZm6_q1kv8baUfn4,32100
5
5
  lsst/resources/davutils.py,sha256=5geEl_44lrWX-Si1VDfYJ6WP1rg22PBqlyD_v1HE4yI,100300
6
+ lsst/resources/eups.py,sha256=imN_qrmtHVQ0g-rCrGNYrlabWBJONRCT02hZkN7ikvc,4056
6
7
  lsst/resources/file.py,sha256=v2XLOzflfhI6kjUGB1mE8p-1e1B2eE58PW-qsQSCqdA,24360
7
8
  lsst/resources/gs.py,sha256=3qMEqO1wIK05BJmuUHtsEunuYWgR4-eB5Z3ffxEtb0o,12827
8
9
  lsst/resources/http.py,sha256=WSx2VXKFd6486TytV2NMfdgLntioL6FvliZWpn9LtDE,92426
9
10
  lsst/resources/location.py,sha256=x3Tq0x5o1OXYmZDxYBenUG1N71wtDhnjVAr3s2ZEiu8,7937
10
11
  lsst/resources/mem.py,sha256=xCpGgvxF2gmO5gLkOikKvIet2RPvaPCiARenR9pUWCk,1115
11
- lsst/resources/packageresource.py,sha256=vnfeRlpVwpC5cDQZE6Lnh8EH6oZy1sH2vLz9ONYjJ4k,6817
12
+ lsst/resources/packageresource.py,sha256=FJ2t7HSnAgtfLUEvg5TTEYrWEXdqVagTmWGfP0hCfC4,6841
13
+ lsst/resources/proxied.py,sha256=9hpfVBbzkC66QNHSKgd4lXBv3XTMzqPwXalkLZv7A34,4775
12
14
  lsst/resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
15
  lsst/resources/s3.py,sha256=NGJPM4BjtqFIPvg9vbp_blrIRt009NbOm06cr65Wqmw,29662
14
16
  lsst/resources/s3utils.py,sha256=ojWf9BPrK9mhGQ8jvs4_8Nsqf9360e79U5FnPTxe24A,14576
15
- lsst/resources/schemeless.py,sha256=9tgqf0eQI3ErGpGSscTRFk_8amF6GwpykPBaTa-KqLI,10909
16
- lsst/resources/tests.py,sha256=UD2Pql8olpW9oCDlsA_jtl23SZtknp7ReuJHLcMPSa0,46237
17
+ lsst/resources/schemeless.py,sha256=Bhcjy-M3k7D-Kmf1ShXYaPqceGb0bOns4Rw9ecXOJjA,11658
18
+ lsst/resources/tests.py,sha256=0tDhVtF1euMH0Y7QR-RbQyo83GPe9baIPI6pM9VyZ-Y,46516
17
19
  lsst/resources/utils.py,sha256=6O3Mq7JbPEtqyD2lM77pRpwcPMfV5SxiNMknw-F2vNs,8097
18
- lsst/resources/version.py,sha256=MHZJr_Yoevn1SeqerO6dmfTP3xtJLGxPmEi-9Jpl380,55
20
+ lsst/resources/version.py,sha256=Tt7y1Knxuicvk026_H8m0GXmxEFM8XEWyfbDoHcAxbs,52
19
21
  lsst/resources/_resourceHandles/__init__.py,sha256=zOcZ8gVEBdAWcHJaZabA8Vdq-wAVcxjbmA_1b1IWM6M,76
20
22
  lsst/resources/_resourceHandles/_baseResourceHandle.py,sha256=lQwxDOmFUNJndTxsjpz-HxrQBL0L-z4aXQocHdOEI7c,4676
21
23
  lsst/resources/_resourceHandles/_davResourceHandle.py,sha256=xcJNFUj8VzlPOKlHdXXoFFyiLNiSFiT-RFNqJRzKniQ,6799
22
24
  lsst/resources/_resourceHandles/_fileResourceHandle.py,sha256=2nC8tfP_ynAfjpzrtkw_1ahx1CuMEFpZ5mLmofSShUk,3676
23
25
  lsst/resources/_resourceHandles/_httpResourceHandle.py,sha256=Yami8IVGeru4bLQCag-OvGG0ltz1qyEg57FY4IEB87Y,10995
24
26
  lsst/resources/_resourceHandles/_s3ResourceHandle.py,sha256=Cp-eBtptskbmthy3DwLKPpYPLvU_lrqtK10X37inHt0,12406
25
- lsst_resources-29.2025.4800.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
26
- lsst_resources-29.2025.4800.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
27
- lsst_resources-29.2025.4800.dist-info/METADATA,sha256=c5UbrCKnTjqtgCZiLq5INC7gvEVVL3rDlzT5Ev1I6PU,2240
28
- lsst_resources-29.2025.4800.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
- lsst_resources-29.2025.4800.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
30
- lsst_resources-29.2025.4800.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
- lsst_resources-29.2025.4800.dist-info/RECORD,,
27
+ lsst_resources-30.0.0rc3.dist-info/licenses/COPYRIGHT,sha256=yazVsoMmFwhiw5itGrdT4YPmXbpsQyUFjlpOyZIa77M,148
28
+ lsst_resources-30.0.0rc3.dist-info/licenses/LICENSE,sha256=7wrtgl8meQ0_RIuv2TjIKpAnNrl-ODH-QLwyHe9citI,1516
29
+ lsst_resources-30.0.0rc3.dist-info/METADATA,sha256=iTiiS1jfRbFhYIVbouFR6TCSnoHjqKYeI64RJElTofY,2237
30
+ lsst_resources-30.0.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ lsst_resources-30.0.0rc3.dist-info/top_level.txt,sha256=eUWiOuVVm9wwTrnAgiJT6tp6HQHXxIhj2QSZ7NYZH80,5
32
+ lsst_resources-30.0.0rc3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
33
+ lsst_resources-30.0.0rc3.dist-info/RECORD,,