Coverage for python / lsst / daf / butler / _labeled_butler_factory.py: 34%

78 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-17 16:22 -0800

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__ = ("LabeledButlerFactory", "LabeledButlerFactoryProtocol") 

31 

32from collections.abc import Mapping 

33from typing import Protocol 

34 

35from lsst.resources import ResourcePathExpression 

36 

37from ._butler import Butler 

38from ._butler_config import ButlerConfig, ButlerType 

39from ._butler_repo_index import ButlerRepoIndex 

40from ._utilities.named_locks import NamedLocks 

41from ._utilities.thread_safe_cache import ThreadSafeCache 

42 

43 

44class LabeledButlerFactoryProtocol(Protocol): 

45 """Callable to retrieve a butler from a label.""" 

46 

47 def __call__(self, label: str) -> Butler: ... 47 ↛ exitline 47 didn't return from function '__call__' because

48 

49 

50class LabeledButlerFactory: 

51 """Factory for efficiently instantiating Butler instances from the 

52 repository index file. This is intended for use from long-lived services 

53 that want to instantiate a separate Butler instance for each end user 

54 request. 

55 

56 Parameters 

57 ---------- 

58 repositories : `~collections.abc.Mapping` [`str`, `str`], optional 

59 Keys are arbitrary labels, and values are URIs to Butler configuration 

60 files. If not provided, defaults to the global repository index 

61 configured by the ``DAF_BUTLER_REPOSITORY_INDEX`` environment variable 

62 -- see `ButlerRepoIndex`. 

63 

64 Notes 

65 ----- 

66 This interface is currently considered experimental and is subject to 

67 change. 

68 

69 For each label in the repository index, caches shared state to allow fast 

70 instantiation of new instances. 

71 

72 Instance methods on this class are threadsafe -- a single instance of 

73 `LabeledButlerFactory` can be used concurrently by multiple threads. It is 

74 NOT safe for a single `Butler` instance returned by this factory to be used 

75 concurrently by multiple threads. However, separate `Butler` instances can 

76 safely be used by separate threads. 

77 """ 

78 

79 def __init__(self, repositories: Mapping[str, str] | None = None) -> None: 

80 if repositories is None: 

81 self._repositories = None 

82 else: 

83 self._repositories = dict(repositories) 

84 

85 self._factories = ThreadSafeCache[str, _ButlerFactory]() 

86 self._initialization_locks = NamedLocks() 

87 

88 # This may be overridden by unit tests. 

89 self._preload_unsafe_direct_butler_caches = True 

90 

91 def bind(self, access_token: str | None) -> LabeledButlerFactoryProtocol: 

92 """Create a callable factory function for generating Butler instances 

93 with out needing to specify access tokans again. 

94 

95 Parameters 

96 ---------- 

97 access_token : `str` or `None` 

98 An optional access token to use for authentication with the Butler. 

99 

100 Returns 

101 ------- 

102 bound : `LabeledButlerFactoryProtocol` 

103 A callable that takes a label as input and returns a Butler 

104 instance. 

105 """ 

106 

107 def create(label: str) -> Butler: 

108 return self.create_butler(label=label, access_token=access_token) 

109 

110 return create 

111 

112 def create_butler(self, *, label: str, access_token: str | None) -> Butler: 

113 """Create a Butler instance. 

114 

115 Parameters 

116 ---------- 

117 label : `str` 

118 Label of the repository to instantiate, from the ``repositories`` 

119 parameter to the `LabeledButlerFactory` constructor or the global 

120 repository index file. 

121 access_token : `str` | `None` 

122 Gafaelfawr access token used to authenticate to a Butler server. 

123 This is required for any repositories configured to use 

124 `RemoteButler`. If you only use `DirectButler`, this may be 

125 `None`. 

126 

127 Raises 

128 ------ 

129 KeyError 

130 Raised if the label is not found in the index. 

131 

132 Notes 

133 ----- 

134 For a service making requests on behalf of end users, the access token 

135 should normally be a "delegated" token so that access permissions are 

136 based on the end user instead of the service. See 

137 https://gafaelfawr.lsst.io/user-guide/gafaelfawringress.html#requesting-delegated-tokens 

138 """ 

139 factory = self._get_or_create_butler_factory(label) 

140 return factory.create_butler(access_token) 

141 

142 def close(self) -> None: 

143 """Reset the factory cache, and release any resources associated with 

144 the cached instances. 

145 """ 

146 factories = self._factories.clear() 

147 for factory in factories.values(): 

148 factory.close() 

149 

150 def _get_or_create_butler_factory(self, label: str) -> _ButlerFactory: 

151 # We maintain a separate lock per label. We only want to instantiate 

152 # one factory function per label, because creating the factory sets up 

153 # shared state that should only exist once per repository. However, we 

154 # don't want other repositories' instance creation to block on one 

155 # repository that is slow to initialize. 

156 with self._initialization_locks.lock(label): 

157 if (factory := self._factories.get(label)) is not None: 

158 return factory 

159 

160 factory = self._create_butler_factory_function(label) 

161 return self._factories.set_or_get(label, factory) 

162 

163 def _create_butler_factory_function(self, label: str) -> _ButlerFactory: 

164 config_uri = self._get_config_uri(label) 

165 config = ButlerConfig(config_uri) 

166 butler_type = config.get_butler_type() 

167 

168 match butler_type: 

169 case ButlerType.DIRECT: 

170 return _DirectButlerFactory(config, self._preload_unsafe_direct_butler_caches) 

171 case ButlerType.REMOTE: 

172 return _RemoteButlerFactory(config) 

173 case _: 

174 raise TypeError(f"Unknown butler type '{butler_type}' for label '{label}'") 

175 

176 def _get_config_uri(self, label: str) -> ResourcePathExpression: 

177 if self._repositories is None: 

178 return ButlerRepoIndex.get_repo_uri(label) 

179 else: 

180 config_uri = self._repositories.get(label) 

181 if config_uri is None: 

182 raise KeyError(f"Unknown repository label '{label}'") 

183 return config_uri 

184 

185 

186class _ButlerFactory(Protocol): 

187 def create_butler(self, access_token: str | None) -> Butler: ... 187 ↛ exitline 187 didn't return from function 'create_butler' because

188 def close(self) -> None: ... 188 ↛ exitline 188 didn't return from function 'close' because

189 

190 

191class _DirectButlerFactory(_ButlerFactory): 

192 def __init__(self, config: ButlerConfig, preload_unsafe_caches: bool) -> None: 

193 import lsst.daf.butler.direct_butler 

194 

195 # Create a 'template' Butler that will be cloned when callers request 

196 # an instance. 

197 self._butler = Butler.from_config(config) 

198 assert isinstance(self._butler, lsst.daf.butler.direct_butler.DirectButler) 

199 

200 # Load caches so that data is available in cloned instances without 

201 # needing to refetch it from the database for every instance. 

202 self._butler._preload_cache(load_dimension_record_cache=preload_unsafe_caches) 

203 

204 def create_butler(self, access_token: str | None) -> Butler: 

205 # Access token is ignored because DirectButler does not use Gafaelfawr 

206 # authentication. 

207 return self._butler.clone() 

208 

209 def close(self) -> None: 

210 self._butler.close() 

211 

212 

213class _RemoteButlerFactory(_ButlerFactory): 

214 def __init__(self, config: ButlerConfig) -> None: 

215 import lsst.daf.butler.remote_butler._factory 

216 

217 self._factory = lsst.daf.butler.remote_butler._factory.RemoteButlerFactory.create_factory_from_config( 

218 config 

219 ) 

220 

221 def create_butler(self, access_token: str | None) -> Butler: 

222 if access_token is None: 

223 raise ValueError("Access token is required to connect to a Butler server") 

224 return self._factory.create_butler_for_access_token(access_token) 

225 

226 def close(self) -> None: 

227 pass