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-05-06 08:30 +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/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ("create_app",) 

31 

32from collections.abc import Awaitable, Callable 

33 

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 

38 

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 

48 

49configure_logging(name="lsst.daf.butler.remote_butler.server") 

50configure_uvicorn_logging() 

51enable_telemetry() 

52 

53 

54def create_app() -> FastAPI: 

55 """Create a Butler server FastAPI application.""" 

56 config = load_config() 

57 

58 app = FastAPI() 

59 

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) 

81 

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)) 

92 

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 

116 

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 ) 

127 

128 return app