Skip to content

Commit

Permalink
Merge pull request #1046 from lsst/tickets/DM-45616
Browse files Browse the repository at this point in the history
DM-45616: Add a way for Butler server to serve static files
  • Loading branch information
dhirving committed Aug 6, 2024
2 parents 106dc9f + 335951d commit faad881
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 0 deletions.
54 changes: 54 additions & 0 deletions python/lsst/daf/butler/remote_butler/server/_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# This file is part of daf_butler.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (http://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# This software is dual licensed under the GNU General Public License and also
# under a 3-clause BSD license. Recipients may choose which of these licenses
# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
# respectively. If you choose the GPL option then the following text applies
# (but note that there is still no warranty even if you opt for BSD instead):
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from __future__ import annotations

import dataclasses
import os


@dataclasses.dataclass(frozen=True)
class ButlerServerConfig:
"""Butler server configuration loaded from environment variables.
Notes
-----
Besides these variables, there is one critical environment variable. The
list of repositories to be hosted must be defined using
``DAF_BUTLER_REPOSITORIES`` or ``DAF_BUTLER_REPOSITORY_INDEX`` -- see
`ButlerRepoIndex`.
"""

static_files_path: str | None
"""Absolute path to a directory of files that will be served to end-users
as static files from the `configs/` HTTP route.
"""


def load_config() -> ButlerServerConfig:
"""Read the Butler server configuration from the environment."""
return ButlerServerConfig(static_files_path=os.environ.get("DAF_BUTLER_SERVER_STATIC_FILES_PATH"))
15 changes: 15 additions & 0 deletions python/lsst/daf/butler/remote_butler/server/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
import safir.dependencies.logger
from fastapi import FastAPI, Request, Response
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
from safir.logging import configure_logging, configure_uvicorn_logging

from ..._exceptions import ButlerUserError
from .._errors import serialize_butler_user_error
from ..server_models import CLIENT_REQUEST_ID_HEADER_NAME, ERROR_STATUS_CODE, ErrorResponseModel
from ._config import load_config
from .handlers._external import external_router
from .handlers._external_query import query_router
from .handlers._internal import internal_router
Expand All @@ -49,6 +51,8 @@

def create_app() -> FastAPI:
"""Create a Butler server FastAPI application."""
config = load_config()

app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)

Expand All @@ -67,6 +71,17 @@ def create_app() -> FastAPI:
)
app.include_router(internal_router)

# If configured to do so, serve a directory of static files via HTTP.
#
# Until we are able to fully transition away from DirectButler for the RSP,
# we need a place to host DirectButler configuration files. Since this
# same configuration is needed for Butler server, it's easier to configure
# in Phalanx if we just host them from Butler server itself.
# This will also host the end-user repository index file for the RSP, for
# lack of a better place to put it.
if config.static_files_path:
app.mount(f"{default_api_path}/configs", StaticFiles(directory=config.static_files_path))

# Any time an exception is returned by a handler, add a log message that
# includes the username and request ID from the client. This will make it
# easier to track down user-reported issues in the logs.
Expand Down
2 changes: 2 additions & 0 deletions python/lsst/daf/butler/tests/server_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,7 @@ def _is_authenticated_endpoint(path: str) -> bool:
return False
if path.endswith("/butler.json"):
return False
if path.startswith("/api/butler/configs"):
return False

return True
12 changes: 12 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os.path
import tempfile
import unittest
import uuid

Expand Down Expand Up @@ -106,6 +107,17 @@ def test_health_check(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["name"], "butler")

def test_static_files(self):
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, "temp.txt"), "w") as fh:
fh.write("test data 123")

with mock_env({"DAF_BUTLER_SERVER_STATIC_FILES_PATH": tmpdir}):
with create_test_server(TESTDIR) as server:
response = server.client.get("/api/butler/configs/temp.txt")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.text, "test data 123")

def test_dimension_universe(self):
universe = self.butler.dimensions
self.assertEqual(universe.namespace, "daf_butler")
Expand Down

0 comments on commit faad881

Please sign in to comment.