Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,26 @@ jobs:
run: |
uv pip install --system --no-deps -v -e .

- name: Start fake-gcs-server
run: |
docker run -d --name fake-gcs-server -p 4443:4443 \
fsouza/fake-gcs-server -scheme http -filesystem-root /tmp/fake-gcs-server

for i in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:4443/storage/v1/b >/dev/null; then
exit 0
fi
sleep 1
done

docker logs fake-gcs-server
exit 1

Comment on lines +71 to +85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do end up keeping this, the testcontainers library is a nice way to embed the docker setup logic in unit tests.

Example: https://github.com/lsst/daf_butler/blob/main/tests_integration/test_docker_container.py

- name: Run tests
env:
S3_ENDPOINT_URL: "https://google.com"
STORAGE_EMULATOR_HOST: "http://127.0.0.1:4443"
GOOGLE_CLOUD_PROJECT: "test-project"
run: |
pytest -r a -v -n 3 --cov=lsst.resources\
--cov=tests --cov-report=xml --cov-report=term --cov-branch \
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.bck
*.pyc
.eggs
.env
*.egg-info
version.py
_build.*
Expand Down
1 change: 1 addition & 0 deletions doc/changes/DM-52947.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a ``ResourcePath.get_info()`` method to provide a general interface for obtaining information about a resource including the size, modification date, and any checksums available.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ select = [
]
extend-select = [
"RUF100", # Warn about unused noqa
"D212", # Docstring starts without newline after quotes.
]

[tool.ruff.lint.isort]
Expand Down
3 changes: 2 additions & 1 deletion python/lsst/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

__all__ = (
"ResourceHandleProtocol",
"ResourceInfo",
"ResourcePath",
"ResourcePathExpression",
)
Expand All @@ -22,5 +23,5 @@
from ._resourceHandles import ResourceHandleProtocol

# Should only expose ResourcePath and its input type alias
from ._resourcePath import ResourcePath, ResourcePathExpression
from ._resourcePath import ResourceInfo, ResourcePath, ResourcePathExpression
from .version import *
4 changes: 2 additions & 2 deletions python/lsst/resources/_resourceHandles/_davResourceHandle.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ class DavReadAheadCache:
Parameters
----------
client : `lsst.resources.davutils.DavClient`
webDAV client to interact with the server to download data.
backend_url : `str`
The webDAV client to interact with the server to download data.
url : `str`
URL of the resource to download data from.
filesize : `int`
Size in bytes of the remote file.
Expand Down
36 changes: 35 additions & 1 deletion python/lsst/resources/_resourcePath.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

from __future__ import annotations

__all__ = ("ResourcePath", "ResourcePathExpression")
__all__ = ("ResourceInfo", "ResourcePath", "ResourcePathExpression")

import concurrent.futures
import contextlib
import copy
import dataclasses
import datetime
import io
import locale
import logging
Expand Down Expand Up @@ -131,6 +133,27 @@ def _patch_environ(new_values: dict[str, str]) -> Iterator[None]:
os.environ[k] = old_values[k]


@dataclasses.dataclass(frozen=True)
class ResourceInfo:
Comment thread
timj marked this conversation as resolved.
"""Information about this resource."""

uri: str
"""URI in string form of the resource from which this information is
derived.
"""
is_file: bool
"""Indicate whether the resource is a file or a directory."""
size: int
"""Size of the file in bytes. A directory or a URI that has no concept
of size returns 0."""
last_modified: datetime.datetime | None
"""Modification date of the resource, if known."""
checksums: dict[str, Any]
"""Checksums for this file. Supported checksum implementations are
backend dependent.
"""


class ResourcePath: # numpydoc ignore=PR02
"""Convenience wrapper around URI parsers.

Expand Down Expand Up @@ -1931,6 +1954,17 @@ def _copy_extra_attributes(self, original_uri: ResourcePath) -> None:
# ResourcePath constructor by passing in a ResourcePath object.
pass

def get_info(self) -> ResourceInfo:
"""Return lightweight metadata about this resource.

Returns
-------
info : `ResourceInfo`
The information about this resource that can be obtained from
the backend. Will not read the file contents.
"""
raise NotImplementedError("")


ResourcePathExpression = str | urllib.parse.ParseResult | ResourcePath | Path
"""Type-annotation alias for objects that can be coerced to ResourcePath.
Expand Down
24 changes: 17 additions & 7 deletions python/lsst/resources/dav.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

from ._resourceHandles import ResourceHandleProtocol
from ._resourceHandles._davResourceHandle import DavReadResourceHandle
from ._resourcePath import ResourcePath, ResourcePathExpression
from ._resourcePath import ResourceInfo, ResourcePath, ResourcePathExpression
from .davutils import (
DavClient,
DavClientPool,
Expand Down Expand Up @@ -139,8 +139,7 @@ def __init__(self) -> None:
self._reset()

def _reset(self) -> None:
"""
Initialize all the globals.
"""Initialize all the globals.

This method is a helper for reinitializing globals in tests.
"""
Expand Down Expand Up @@ -289,11 +288,22 @@ def size(self) -> int:

return 0 if self.isdir() else self._client.size(self._internal_url)

def info(self) -> dict[str, Any]:
"""Return metadata details about this resource."""
log.debug("info %s [%#x]", self, id(self))
@override
def get_info(self) -> ResourceInfo:
Comment thread
timj marked this conversation as resolved.
"""Return lightweight metadata details about this resource."""
log.debug("get_info %s [%#x]", self, id(self))

info = self._client.info(self._internal_url)
if info["type"] is None:
raise FileNotFoundError(f"Resource {self} does not exist")

return self._client.info(self._internal_url, name=str(self))
return ResourceInfo(
uri=str(self),
is_file=info["type"] == "file",
size=info["size"],
last_modified=info["last_modified"],
checksums=info["checksums"],
)

@override
def read(self, size: int = -1) -> bytes:
Expand Down
Loading
Loading