Coverage for python / lsst / daf / butler / remote_butler / server / _server.py: 0%
46 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:48 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:48 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = ("create_app",)
32from collections.abc import Awaitable, Callable
34import safir.dependencies.logger
35from fastapi import Depends, FastAPI, Request, Response
36from fastapi.staticfiles import StaticFiles
37from safir.logging import configure_logging, configure_uvicorn_logging
39from ..._exceptions import ButlerUserError
40from .._errors import serialize_butler_user_error
41from ..server_models import CLIENT_REQUEST_ID_HEADER_NAME, ERROR_STATUS_CODE, ErrorResponseModel
42from ._config import load_config
43from ._dependencies import repository_authorization_dependency
44from ._telemetry import enable_telemetry
45from .handlers._external import external_router, unauthenticated_external_router
46from .handlers._external_query import query_router
47from .handlers._internal import internal_router
49configure_logging(name="lsst.daf.butler.remote_butler.server")
50configure_uvicorn_logging()
51enable_telemetry()
54def create_app() -> FastAPI:
55 """Create a Butler server FastAPI application."""
56 config = load_config()
58 app = FastAPI()
60 # A single instance of the server can serve data from multiple Butler
61 # repositories. This 'repository' path placeholder is consumed by
62 # factory_dependency().
63 default_api_path = "/api/butler"
64 prefix = f"{default_api_path}/repo/{{repository}}"
65 auth_dependencies = [Depends(repository_authorization_dependency)]
66 if not config.gafaelfawr_enabled:
67 auth_dependencies = []
68 for router in (external_router, query_router):
69 app.include_router(
70 router,
71 prefix=prefix,
72 # Verify that users have permission to access repository-specific
73 # resources.
74 dependencies=auth_dependencies,
75 # document that 422 responses will include a JSON-formatted error
76 # message, from `butler_exception_handler()` below.
77 responses={422: {"model": ErrorResponseModel}},
78 )
79 app.include_router(unauthenticated_external_router, prefix=prefix)
80 app.include_router(internal_router)
82 # If configured to do so, serve a directory of static files via HTTP.
83 #
84 # Until we are able to fully transition away from DirectButler for the RSP,
85 # we need a place to host DirectButler configuration files. Since this
86 # same configuration is needed for Butler server, it's easier to configure
87 # in Phalanx if we just host them from Butler server itself.
88 # This will also host the end-user repository index file for the RSP, for
89 # lack of a better place to put it.
90 if config.static_files_path:
91 app.mount(f"{default_api_path}/configs", StaticFiles(directory=config.static_files_path))
93 # Any time an exception is returned by a handler, add a log message that
94 # includes the username and request ID from the client. This will make it
95 # easier to track down user-reported issues in the logs.
96 #
97 # This middleware is higher in the middleware stack than FastAPI's
98 # HttpException and ValidationError handling middleware, so it only
99 # catches unhandled errors that would result in a 500 Internal
100 # Server Error.
101 @app.middleware("http")
102 async def _log_exceptions_middleware(
103 request: Request, call_next: Callable[[Request], Awaitable[Response]]
104 ) -> Response:
105 try:
106 return await call_next(request)
107 except Exception as e:
108 logger = await safir.dependencies.logger.logger_dependency(request)
109 await logger.aerror(
110 "Exception",
111 exc_info=e,
112 clientRequestId=request.headers.get(CLIENT_REQUEST_ID_HEADER_NAME, "(unknown)"),
113 user=request.headers.get("X-Auth-Request-User", "(unknown)"),
114 )
115 raise
117 # Configure FastAPI to forward to the client any exceptions tagged as
118 # user-facing Butler errors.
119 @app.exception_handler(ButlerUserError)
120 async def butler_exception_handler(request: Request, exc: ButlerUserError) -> Response:
121 error = serialize_butler_user_error(exc)
122 return Response(
123 media_type="application/json",
124 status_code=ERROR_STATUS_CODE,
125 content=error.model_dump_json(),
126 )
128 return app