lsst-resources 29.2025.4800__tar.gz → 30.0.0__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.4800/python/lsst_resources.egg-info → lsst_resources-30.0.0}/PKG-INFO +1 -1
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/doc/lsst.resources/CHANGES.rst +30 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourcePath.py +15 -1
- lsst_resources-30.0.0/python/lsst/resources/eups.py +101 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/packageresource.py +2 -0
- lsst_resources-30.0.0/python/lsst/resources/proxied.py +148 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/schemeless.py +17 -5
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/tests.py +14 -3
- lsst_resources-30.0.0/python/lsst/resources/version.py +2 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0/python/lsst_resources.egg-info}/PKG-INFO +1 -1
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/SOURCES.txt +3 -0
- lsst_resources-30.0.0/tests/test_eups.py +239 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_file.py +4 -4
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_s3.py +6 -0
- lsst_resources-29.2025.4800/python/lsst/resources/version.py +0 -2
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/COPYRIGHT +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/LICENSE +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/MANIFEST.in +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/README.md +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/doc/lsst.resources/dav.rst +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/doc/lsst.resources/index.rst +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/doc/lsst.resources/internal-api.rst +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/doc/lsst.resources/s3.rst +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/pyproject.toml +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/__init__.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/__init__.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourceHandles/__init__.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourceHandles/_baseResourceHandle.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourceHandles/_davResourceHandle.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourceHandles/_fileResourceHandle.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourceHandles/_httpResourceHandle.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourceHandles/_s3ResourceHandle.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/dav.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/davutils.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/file.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/gs.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/http.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/location.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/mem.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/py.typed +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/s3.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/s3utils.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/utils.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/dependency_links.txt +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/requires.txt +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/top_level.txt +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/zip-safe +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/setup.cfg +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_dav.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_gs.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_http.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_location.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_mem.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_resource.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_s3utils.py +0 -0
- {lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/tests/test_schemeless.py +0 -0
{lsst_resources-29.2025.4800/python/lsst_resources.egg-info → lsst_resources-30.0.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-resources
|
|
3
|
-
Version:
|
|
3
|
+
Version: 30.0.0
|
|
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,3 +1,33 @@
|
|
|
1
|
+
Resources v30.0.0 (2026-01-15)
|
|
2
|
+
==============================
|
|
3
|
+
|
|
4
|
+
New Features
|
|
5
|
+
------------
|
|
6
|
+
|
|
7
|
+
- Added ``walk_up`` parameter to ``ResourcePath.relative_to`` to match the behavior of ``Path.relative_to``.
|
|
8
|
+
Using ``walk_up=True`` allows ``..`` to be present in the returned string.
|
|
9
|
+
The default is `False` to remain compatible with previous versions.
|
|
10
|
+
This parameter can only be `True` for Python 3.12 and newer. (`DM-41256 <https://rubinobs.atlassian.net/browse/DM-41256>`_)
|
|
11
|
+
- Added ``eups:`` URI scheme.
|
|
12
|
+
This scheme will use the EUPS ``$<PRODUCT>_DIR`` environment variables if set, else try to find the corresponding Python package and look for a ``resources`` directory inside it following the decisions made in `RFC-1090 <https://rubinobs.atlassian.net/browse/RFC-1090>`_.
|
|
13
|
+
Additionally, if a URI starts with ``$PRODUCT_DIR`` it is automatically converted to an EUPS URI. (`DM-50997 <https://rubinobs.atlassian.net/browse/DM-50997>`_)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Bug Fixes
|
|
17
|
+
---------
|
|
18
|
+
|
|
19
|
+
- Fixed ``ResourcePath.write`` for schemeless URIs that had no directory component. (`DM-41256 <https://rubinobs.atlassian.net/browse/DM-41256>`_)
|
|
20
|
+
- Fixed an issue where ``S3ResourcePath.transfer_from(S3ResourcePath)`` would fail when the source and destination were using different S3 endpoints or sets of credentials. (`DM-51384 <https://rubinobs.atlassian.net/browse/DM-51384>`_)
|
|
21
|
+
- Two simultaneous copies without overwrite could both succeed in certain cases.
|
|
22
|
+
This race condition has now been eliminated. (`DM-51540 <https://rubinobs.atlassian.net/browse/DM-51540>`_)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Miscellaneous Changes of Minor Interest
|
|
26
|
+
---------------------------------------
|
|
27
|
+
|
|
28
|
+
- Fixed bug where ``.parents()`` and ``.dirname()`` were incorrectly returning the fragment of the file. (`DM-51890 <https://rubinobs.atlassian.net/browse/DM-51890>`_)
|
|
29
|
+
|
|
30
|
+
|
|
1
31
|
Resources v29.1.0 (2025-06-13)
|
|
2
32
|
==============================
|
|
3
33
|
|
{lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst/resources/_resourcePath.py
RENAMED
|
@@ -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.
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-29.2025.4800 → lsst_resources-30.0.0/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:
|
|
3
|
+
Version: 30.0.0
|
|
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
|
{lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/SOURCES.txt
RENAMED
|
@@ -14,12 +14,14 @@ python/lsst/resources/__init__.py
|
|
|
14
14
|
python/lsst/resources/_resourcePath.py
|
|
15
15
|
python/lsst/resources/dav.py
|
|
16
16
|
python/lsst/resources/davutils.py
|
|
17
|
+
python/lsst/resources/eups.py
|
|
17
18
|
python/lsst/resources/file.py
|
|
18
19
|
python/lsst/resources/gs.py
|
|
19
20
|
python/lsst/resources/http.py
|
|
20
21
|
python/lsst/resources/location.py
|
|
21
22
|
python/lsst/resources/mem.py
|
|
22
23
|
python/lsst/resources/packageresource.py
|
|
24
|
+
python/lsst/resources/proxied.py
|
|
23
25
|
python/lsst/resources/py.typed
|
|
24
26
|
python/lsst/resources/s3.py
|
|
25
27
|
python/lsst/resources/s3utils.py
|
|
@@ -40,6 +42,7 @@ python/lsst_resources.egg-info/requires.txt
|
|
|
40
42
|
python/lsst_resources.egg-info/top_level.txt
|
|
41
43
|
python/lsst_resources.egg-info/zip-safe
|
|
42
44
|
tests/test_dav.py
|
|
45
|
+
tests/test_eups.py
|
|
43
46
|
tests/test_file.py
|
|
44
47
|
tests/test_gs.py
|
|
45
48
|
tests/test_http.py
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import unittest
|
|
16
|
+
import unittest.mock
|
|
17
|
+
|
|
18
|
+
from lsst.resources import ResourcePath
|
|
19
|
+
from lsst.resources.eups import EupsResourcePath
|
|
20
|
+
from lsst.resources.tests import GenericTestCase
|
|
21
|
+
|
|
22
|
+
TESTDIR = os.path.abspath(os.path.dirname(__file__))
|
|
23
|
+
PKG_DIR = os.path.join(TESTDIR, "packages", "eups")
|
|
24
|
+
PACKAGE_PATH = os.path.join(TESTDIR, "packages")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EupsTestCase(GenericTestCase, unittest.TestCase):
|
|
28
|
+
"""Generic test of resource URIs."""
|
|
29
|
+
|
|
30
|
+
scheme = "eups"
|
|
31
|
+
netloc = "pkg"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def setUpClass(cls) -> None:
|
|
35
|
+
# The actual value does not matter for these tests.
|
|
36
|
+
os.environ["PKG_DIR"] = PKG_DIR
|
|
37
|
+
super().setUpClass()
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def tearDownClass(cls) -> None:
|
|
41
|
+
del os.environ["PKG_DIR"]
|
|
42
|
+
super().tearDownClass()
|
|
43
|
+
|
|
44
|
+
def test_relative(self) -> None:
|
|
45
|
+
# This test uses two additional netlocs which need corresponding
|
|
46
|
+
# environment variables to function. The values do not matter.
|
|
47
|
+
with unittest.mock.patch.dict(os.environ, {"OTHER_DIR": "x", "MY.HOST_DIR": "y"}):
|
|
48
|
+
super().test_relative()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EupsReadTestCase(unittest.TestCase):
|
|
52
|
+
"""Test that EUPS information can be read.
|
|
53
|
+
|
|
54
|
+
EUPS resources can be thought of as being read only even if the
|
|
55
|
+
underlying URI is a ``file`` URI.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
scheme = "eups"
|
|
59
|
+
netloc = "pkg"
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def setUpClass(cls) -> None:
|
|
63
|
+
# The actual value does not matter for these tests.
|
|
64
|
+
os.environ["PKG_DIR"] = PKG_DIR
|
|
65
|
+
super().setUpClass()
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def tearDownClass(cls) -> None:
|
|
69
|
+
del os.environ["PKG_DIR"]
|
|
70
|
+
super().tearDownClass()
|
|
71
|
+
|
|
72
|
+
def setUp(self):
|
|
73
|
+
self.root = f"{self.scheme}://{self.netloc}"
|
|
74
|
+
self.root_uri = ResourcePath(self.root)
|
|
75
|
+
|
|
76
|
+
def test_read(self):
|
|
77
|
+
uri = self.root_uri.join("config/test.txt")
|
|
78
|
+
self.assertTrue(uri.exists(), f"Check {uri} exists")
|
|
79
|
+
|
|
80
|
+
content = uri.read().decode()
|
|
81
|
+
self.assertIn("A test config.", content)
|
|
82
|
+
|
|
83
|
+
with uri.as_local() as local_uri:
|
|
84
|
+
self.assertEqual(local_uri.scheme, "file")
|
|
85
|
+
self.assertTrue(local_uri.exists())
|
|
86
|
+
|
|
87
|
+
truncated = uri.read(size=9).decode()
|
|
88
|
+
self.assertEqual(truncated, content[:9])
|
|
89
|
+
|
|
90
|
+
# Check that directory determination can work directly without the
|
|
91
|
+
# trailing slash.
|
|
92
|
+
d = self.root_uri.join("config")
|
|
93
|
+
self.assertTrue(d.isdir())
|
|
94
|
+
self.assertTrue(d.dirLike)
|
|
95
|
+
|
|
96
|
+
d = self.root_uri.join("config/", forceDirectory=True)
|
|
97
|
+
self.assertTrue(d.exists(), f"Check directory {d} exists")
|
|
98
|
+
self.assertTrue(d.isdir())
|
|
99
|
+
|
|
100
|
+
with self.assertRaises(IsADirectoryError):
|
|
101
|
+
with d.as_local() as local_uri:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
j = d.join("test.txt")
|
|
105
|
+
self.assertEqual(uri, j)
|
|
106
|
+
self.assertFalse(j.dirLike)
|
|
107
|
+
self.assertFalse(j.isdir())
|
|
108
|
+
not_there = d.join("not-there.yaml")
|
|
109
|
+
self.assertFalse(not_there.exists())
|
|
110
|
+
|
|
111
|
+
bad = ResourcePath(f"{self.scheme}://bad.module/not.yaml")
|
|
112
|
+
multi = ResourcePath.mexists([uri, bad, not_there])
|
|
113
|
+
self.assertTrue(multi[uri])
|
|
114
|
+
self.assertFalse(multi[bad])
|
|
115
|
+
self.assertFalse(multi[not_there])
|
|
116
|
+
|
|
117
|
+
# Check that the bad URI works as expected.
|
|
118
|
+
self.assertFalse(bad.exists())
|
|
119
|
+
self.assertFalse(bad.isdir())
|
|
120
|
+
with self.assertRaises(FileNotFoundError):
|
|
121
|
+
bad.read()
|
|
122
|
+
with self.assertRaises(FileNotFoundError):
|
|
123
|
+
with bad.as_local():
|
|
124
|
+
pass
|
|
125
|
+
with self.assertRaises(FileNotFoundError):
|
|
126
|
+
with bad.open("r"):
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
# fsspec is always not implemented.
|
|
130
|
+
with self.assertRaises(NotImplementedError):
|
|
131
|
+
bad.to_fsspec()
|
|
132
|
+
|
|
133
|
+
def test_open(self):
|
|
134
|
+
uri = self.root_uri.join("config/test.txt")
|
|
135
|
+
with uri.open("rb") as buffer:
|
|
136
|
+
content = buffer.read()
|
|
137
|
+
self.assertEqual(uri.read(), content)
|
|
138
|
+
|
|
139
|
+
with uri.open("r") as buffer:
|
|
140
|
+
content = buffer.read()
|
|
141
|
+
self.assertEqual(uri.read().decode(), content)
|
|
142
|
+
|
|
143
|
+
def test_walk(self):
|
|
144
|
+
"""Test that we can find file resources.
|
|
145
|
+
|
|
146
|
+
Try to find resources in this package. Python does not care whether
|
|
147
|
+
a resource is a Python file or anything else.
|
|
148
|
+
"""
|
|
149
|
+
resource = ResourcePath(f"{self.scheme}://{self.netloc}/")
|
|
150
|
+
resources = set(ResourcePath.findFileResources([resource]))
|
|
151
|
+
|
|
152
|
+
# Do not try to list all possible options. Files can move around
|
|
153
|
+
# and cache files can appear.
|
|
154
|
+
subset = {
|
|
155
|
+
ResourcePath(f"{self.scheme}://{self.netloc}/config/test.txt"),
|
|
156
|
+
ResourcePath(f"{self.scheme}://{self.netloc}/config/test2.yaml"),
|
|
157
|
+
}
|
|
158
|
+
for r in subset:
|
|
159
|
+
self.assertIn(r, resources)
|
|
160
|
+
|
|
161
|
+
resources = set(
|
|
162
|
+
ResourcePath.findFileResources(
|
|
163
|
+
[ResourcePath(f"{self.scheme}://{self.netloc}/")], file_filter=r".*\.json"
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
self.assertEqual(resources, set())
|
|
167
|
+
|
|
168
|
+
# Compare regex with str.
|
|
169
|
+
regex = r".*\.yaml"
|
|
170
|
+
y_files_str = list(resource.walk(file_filter=regex))
|
|
171
|
+
y_files_re = list(resource.walk(file_filter=re.compile(regex)))
|
|
172
|
+
self.assertGreater(len(y_files_str), 1)
|
|
173
|
+
self.assertEqual(y_files_str, y_files_re)
|
|
174
|
+
|
|
175
|
+
bad_dir = ResourcePath(f"{self.scheme}://bad.module/a/dir/")
|
|
176
|
+
self.assertTrue(bad_dir.isdir())
|
|
177
|
+
with self.assertRaises(ValueError):
|
|
178
|
+
list(bad_dir.walk())
|
|
179
|
+
|
|
180
|
+
def test_env_var(self):
|
|
181
|
+
"""Test that environment variables are converted."""
|
|
182
|
+
with unittest.mock.patch.dict(os.environ, {"MY_TEST_DIR": TESTDIR}):
|
|
183
|
+
for env_string in ("$MY_TEST_DIR", "${MY_TEST_DIR}"):
|
|
184
|
+
uri = ResourcePath(f"{env_string}/data/dir1/a.yaml")
|
|
185
|
+
self.assertEqual(uri.path, "/data/dir1/a.yaml")
|
|
186
|
+
self.assertEqual(uri.scheme, "eups")
|
|
187
|
+
self.assertEqual(uri.netloc, "my_test")
|
|
188
|
+
self.assertTrue(uri.exists())
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class EupsAsResourcesReadTestCase(EupsReadTestCase):
|
|
192
|
+
"""Test that EUPS information can be read via resources.
|
|
193
|
+
|
|
194
|
+
EUPS resources can be thought of as being read only even if the
|
|
195
|
+
underlying URI is a ``file`` URI.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
scheme = "eups"
|
|
199
|
+
netloc = "pkg1"
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def setUpClass(cls) -> None:
|
|
203
|
+
# The actual value does not matter for these tests.
|
|
204
|
+
sys.path.append(PACKAGE_PATH)
|
|
205
|
+
super().setUpClass()
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def tearDownClass(cls) -> None:
|
|
209
|
+
sys.path.remove(PACKAGE_PATH)
|
|
210
|
+
super().tearDownClass()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class EupsAsResourcesReadTestCase2(EupsAsResourcesReadTestCase):
|
|
214
|
+
"""Test that EUPS information can be read via resources with lsst-style
|
|
215
|
+
package.
|
|
216
|
+
|
|
217
|
+
EUPS resources can be thought of as being read only even if the
|
|
218
|
+
underlying URI is a ``file`` URI.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
scheme = "eups"
|
|
222
|
+
netloc = "pkg2_sub"
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def setUpClass(cls) -> None:
|
|
226
|
+
# To avoid confusion with other lsst packages, override the default
|
|
227
|
+
# prefix that is added to the EUPS name.
|
|
228
|
+
cls.prefix = EupsResourcePath._default_namespace
|
|
229
|
+
EupsResourcePath._default_namespace = "prefix"
|
|
230
|
+
super().setUpClass()
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def tearDownClass(cls) -> None:
|
|
234
|
+
EupsResourcePath._default_namespace = cls.prefix
|
|
235
|
+
super().tearDownClass()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
unittest.main()
|
|
@@ -46,14 +46,14 @@ class FileTestCase(GenericTestCase, unittest.TestCase):
|
|
|
46
46
|
|
|
47
47
|
def test_env_var(self):
|
|
48
48
|
"""Test that environment variables are expanded."""
|
|
49
|
-
with unittest.mock.patch.dict(os.environ, {"
|
|
50
|
-
uri = ResourcePath("${
|
|
49
|
+
with unittest.mock.patch.dict(os.environ, {"MY_TEST_DIRX": "/a/b/c"}):
|
|
50
|
+
uri = ResourcePath("${MY_TEST_DIRX}/d.txt")
|
|
51
51
|
self.assertEqual(uri.path, "/a/b/c/d.txt")
|
|
52
52
|
self.assertEqual(uri.scheme, "file")
|
|
53
53
|
|
|
54
54
|
# This will not expand
|
|
55
|
-
uri = ResourcePath("${
|
|
56
|
-
self.assertEqual(uri.path, "${
|
|
55
|
+
uri = ResourcePath("${MY_TEST_DIRX}/d.txt", forceAbsolute=False)
|
|
56
|
+
self.assertEqual(uri.path, "${MY_TEST_DIRX}/d.txt")
|
|
57
57
|
self.assertFalse(uri.scheme)
|
|
58
58
|
|
|
59
59
|
def test_ospath(self):
|
|
@@ -224,6 +224,9 @@ class S3ReadWriteTestCaseBase(GenericReadWriteTestCase):
|
|
|
224
224
|
def test_implicit_default_threading(self):
|
|
225
225
|
S3ResourcePath.use_threads = None
|
|
226
226
|
boto_default = signature(boto3.s3.transfer.TransferConfig).parameters["use_threads"].default
|
|
227
|
+
# Newer versions of boto return None as the default.
|
|
228
|
+
if boto_default is None:
|
|
229
|
+
boto_default = True
|
|
227
230
|
test_resource_path = self.root_uri.join("test_file.dat")
|
|
228
231
|
self.assertEqual(test_resource_path._transfer_config.use_threads, boto_default)
|
|
229
232
|
|
|
@@ -231,6 +234,9 @@ class S3ReadWriteTestCaseBase(GenericReadWriteTestCase):
|
|
|
231
234
|
with mock.patch.dict(os.environ, {"LSST_S3_USE_THREADS": "None"}):
|
|
232
235
|
S3ResourcePath.use_threads = None
|
|
233
236
|
boto_default = signature(boto3.s3.transfer.TransferConfig).parameters["use_threads"].default
|
|
237
|
+
# Newer versions of boto return None as the default.
|
|
238
|
+
if boto_default is None:
|
|
239
|
+
boto_default = True
|
|
234
240
|
test_resource_path = self.root_uri.join("test_file.dat")
|
|
235
241
|
self.assertEqual(test_resource_path._transfer_config.use_threads, boto_default)
|
|
236
242
|
|
|
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
|
|
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
|
|
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.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/requires.txt
RENAMED
|
File without changes
|
{lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/python/lsst_resources.egg-info/top_level.txt
RENAMED
|
File without changes
|
{lsst_resources-29.2025.4800 → lsst_resources-30.0.0}/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
|