Skip to content

Commit

Permalink
Provide a Butler configuration from the server
Browse files Browse the repository at this point in the history
  • Loading branch information
dhirving committed Nov 2, 2023
1 parent cf236e2 commit 8b32a96
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 0 deletions.
11 changes: 11 additions & 0 deletions python/lsst/daf/butler/remote_butler/_remote_butler.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import httpx
from lsst.daf.butler import __version__
from lsst.daf.butler.repo_relocation import replaceRoot
from lsst.resources import ResourcePath, ResourcePathExpression
from lsst.utils.introspection import get_full_type_name

Expand Down Expand Up @@ -69,6 +70,16 @@ def __init__(
**kwargs: Any,
):
butler_config = ButlerConfig(config, searchPaths, without_datastore=True)
# There is a convention in Butler config files where <butlerRoot> in a
# configuration option refers to the directory containing the
# configuration file. We allow this for the remote butler's URL so
# that the server doesn't have to know which hostname it is being
# accessed from
server_url_key = ("remote_butler", "url")
if server_url_key in butler_config:
butler_config[server_url_key] = replaceRoot(
butler_config[server_url_key], butler_config.configDir
)
self._config = RemoteButlerConfigModel.model_validate(butler_config)
self._dimensions: DimensionUniverse | None = None
# TODO: RegistryDefaults should have finish() called on it, but this
Expand Down
24 changes: 24 additions & 0 deletions python/lsst/daf/butler/remote_butler/server/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,30 @@ async def get_index() -> Metadata:
return get_metadata(package_name="lsst.daf.butler", application_name="butler")


@app.get(
"/butler/butler.yaml",
description=(
"Returns a Butler YAML configuration file that can be used to instantiate a Butler client"
" pointing at this server"
),
summary="Client configuration file",
response_model=dict[str, Any],
)
@app.get(
"/butler/butler.json",
description=(
"Returns a Butler JSON configuration file that can be used to instantiate a Butler client"
" pointing at this server"
),
summary="Client configuration file",
response_model=dict[str, Any],
)
async def get_client_config() -> dict[str, Any]:
# We can return JSON data for both the YAML and JSON case because all JSON
# files are parseable as YAML.
return {"cls": "lsst.daf.butler.remote_butler.RemoteButler", "remote_butler": {"url": "<butlerRoot>"}}


@app.get("/butler/v1/universe", response_model=dict[str, Any])
def get_dimension_universe(factory: Factory = Depends(factory_dependency)) -> dict[str, Any]:
"""Allow remote client to get dimensions definition."""
Expand Down
26 changes: 26 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@
TestClient = None
app = None

from unittest.mock import patch

from lsst.daf.butler import Butler
from lsst.daf.butler.tests.utils import MetricTestRepo, makeTestTempDir, removeTestTempDir
from lsst.resources.http import HttpResourcePath

TESTDIR = os.path.abspath(os.path.dirname(__file__))

Expand Down Expand Up @@ -96,6 +99,29 @@ def test_remote_butler(self):
universe = self.butler.dimensions
self.assertEqual(universe.namespace, "daf_butler")

def test_instantiate_via_butler_http_search(self):
"""Ensure that the primary Butler constructor's automatic search logic
correctly locates and reads the configuration file and ends up with a
RemoteButler pointing to the correct URL
"""

# This is kind of a fragile test. Butler's search logic does a lot of
# manipulations involving creating new ResourcePaths, and ResourcePath
# doesn't use httpx so we can't easily inject the TestClient in there.
# We don't have an actual valid HTTP URL to give to the constructor
# because the test instance of the server is accessed via ASGI.
#
# Instead we just monkeypatch the HTTPResourcePath 'read' method and
# hope that all ResourcePath HTTP reads during construction are going
# to the server under test.
def override_read(http_resource_path):
return self.client.get(http_resource_path.geturl()).content

with patch.object(HttpResourcePath, "read", override_read):
butler = Butler("https://test.example/butler")
assert isinstance(butler, RemoteButler)
assert str(butler._config.remote_butler.url) == "https://test.example/butler/"


if __name__ == "__main__":
unittest.main()

0 comments on commit 8b32a96

Please sign in to comment.