Coverage for python/lsst/daf/butler/registry/obscore/_spatial.py: 58%

35 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-03 02:48 -0700

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__ = ["MissingDatabaseError", "RegionTypeWarning", "SpatialObsCorePlugin"] 

31 

32from abc import ABC, abstractmethod 

33from collections.abc import Mapping, Sequence 

34from typing import TYPE_CHECKING, Any 

35 

36from lsst.sphgeom import Region 

37from lsst.utils import doImportType 

38 

39if TYPE_CHECKING: 

40 from ... import ddl 

41 from ..interfaces import Database 

42 from ._config import SpatialPluginConfig 

43 from ._records import Record 

44 

45 

46class MissingDatabaseError(Exception): 

47 """Exception raised when database is not provided but plugin implementation 

48 requires it. 

49 """ 

50 

51 

52class RegionTypeError(TypeError): 

53 """Exception raised for unsupported region types.""" 

54 

55 

56class RegionTypeWarning(Warning): 

57 """Warning category for unsupported region types.""" 

58 

59 

60class SpatialObsCorePlugin(ABC): 

61 """Interface for classes that implement support for spatial columns and 

62 indices in obscore table. 

63 """ 

64 

65 @classmethod 

66 @abstractmethod 

67 def initialize(cls, *, name: str, config: Mapping[str, Any], db: Database | None) -> SpatialObsCorePlugin: 

68 """Construct an instance of the plugin. 

69 

70 Parameters 

71 ---------- 

72 name : `str` 

73 Arbitrary name given to this plugin (usually key in 

74 configuration). 

75 config : `dict` [ `str`, `Any` ] 

76 Plugin configuration dictionary. 

77 db : `Database`, optional 

78 Interface to the underlying database engine and namespace. In some 

79 contexts the database may not be available and will be set to 

80 `None`. If plugin class requires access to the database, it should 

81 raise a `MissingDatabaseError` exception when ``db`` is `None`. 

82 

83 Returns 

84 ------- 

85 manager : `ObsCoreTableManager` 

86 Plugin instance. 

87 

88 Raises 

89 ------ 

90 MissingDatabaseError 

91 Raised if plugin requires access to database, but ``db`` is `None`. 

92 """ 

93 raise NotImplementedError() 

94 

95 @abstractmethod 

96 def extend_table_spec(self, table_spec: ddl.TableSpec) -> None: 

97 """Update obscore table specification with any new columns that are 

98 needed for this plugin. 

99 

100 Parameters 

101 ---------- 

102 table_spec : `ddl.TableSpec` 

103 ObsCore table specification. 

104 

105 Notes 

106 ----- 

107 Table specification is updated in place. Plugins should not remove 

108 columns, normally updates are limited to adding new columns or indices. 

109 """ 

110 raise NotImplementedError() 

111 

112 @abstractmethod 

113 def make_records(self, region: Region | None) -> Record | None: 

114 """Return data for obscore records corresponding to a given region. 

115 

116 Parameters 

117 ---------- 

118 region : `Region`, optional 

119 Spatial region, can be `None` if dataset has no associated region. 

120 

121 Returns 

122 ------- 

123 record : `dict` [ `str`, `Any` ] or `None` 

124 Data to store in the main obscore table with column values 

125 corresponding to a region or `None` if there is nothing to store. 

126 

127 Raises 

128 ------ 

129 RegionTypeError 

130 Raised if type of the region is not supported. 

131 """ 

132 raise NotImplementedError() 

133 

134 @classmethod 

135 def load_plugins( 

136 cls, config: Mapping[str, SpatialPluginConfig], db: Database | None 

137 ) -> Sequence[SpatialObsCorePlugin]: 

138 """Load all plugins based on plugin configurations. 

139 

140 Parameters 

141 ---------- 

142 config : `~collections.abc.Mapping` [ `str`, `SpatialPluginConfig` ] 

143 Configuration for plugins. The key is an arbitrary name and the 

144 value is an object describing plugin class and its configuration 

145 options. 

146 db : `Database`, optional 

147 Interface to the underlying database engine and namespace. 

148 

149 Raises 

150 ------ 

151 MissingDatabaseError 

152 Raised if one of the plugins requires access to database, but 

153 ``db`` is `None`. 

154 """ 

155 # We have to always load default plugin even if it does not appear in 

156 # configuration. 

157 from .default_spatial import DefaultSpatialObsCorePlugin 

158 

159 spatial_plugins: list[SpatialObsCorePlugin] = [] 

160 has_default = False 

161 for plugin_name, plugin_config in config.items(): 

162 class_name = plugin_config.cls 

163 plugin_class = doImportType(class_name) 

164 if not issubclass(plugin_class, SpatialObsCorePlugin): 

165 raise TypeError( 

166 f"Spatial ObsCore plugin {class_name} is not a subclass of SpatialObsCorePlugin" 

167 ) 

168 plugin = plugin_class.initialize(name=plugin_name, config=plugin_config.config, db=db) 

169 spatial_plugins.append(plugin) 

170 if plugin_class is DefaultSpatialObsCorePlugin: 

171 has_default = True 

172 

173 if not has_default: 

174 # Always create default spatial plugin. 

175 spatial_plugins.insert( 

176 0, DefaultSpatialObsCorePlugin.initialize(name="default", config={}, db=db) 

177 ) 

178 

179 return spatial_plugins