Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22"""The default concrete implementation of the class that manages 

23attributes for `Registry`. 

24""" 

25 

26__all__ = ["DefaultButlerAttributeManager"] 

27 

28from typing import ( 

29 ClassVar, 

30 Iterable, 

31 Optional, 

32 Tuple, 

33) 

34 

35import sqlalchemy 

36 

37from ..core.ddl import TableSpec, FieldSpec 

38from .interfaces import ( 

39 Database, 

40 ButlerAttributeExistsError, 

41 ButlerAttributeManager, 

42 StaticTablesContext, 

43 VersionTuple 

44) 

45 

46 

47class MissingAttributesTableError(RuntimeError): 

48 """Exception raised when a database is missing attributes table. 

49 """ 

50 pass 

51 

52 

53# This manager is supposed to have super-stable schema that never changes 

54# but there may be cases when we need data migration on this table so we 

55# keep version for it as well. 

56_VERSION = VersionTuple(1, 0, 0) 

57 

58 

59class DefaultButlerAttributeManager(ButlerAttributeManager): 

60 """An implementation of `ButlerAttributeManager` that stores attributes 

61 in a database table. 

62 

63 Parameters 

64 ---------- 

65 db : `Database` 

66 Database engine interface for the namespace in which this table lives. 

67 table : `sqlalchemy.schema.Table` 

68 SQLAlchemy representation of the table that stores attributes. 

69 """ 

70 def __init__(self, db: Database, table: sqlalchemy.schema.Table): 

71 self._db = db 

72 self._table = table 

73 

74 _TABLE_NAME: ClassVar[str] = "butler_attributes" 

75 

76 _TABLE_SPEC: ClassVar[TableSpec] = TableSpec( 

77 fields=[ 

78 FieldSpec("name", dtype=sqlalchemy.String, length=1024, primaryKey=True), 

79 FieldSpec("value", dtype=sqlalchemy.String, length=65535, nullable=False), 

80 ], 

81 ) 

82 

83 @classmethod 

84 def initialize(cls, db: Database, context: StaticTablesContext) -> ButlerAttributeManager: 

85 # Docstring inherited from ButlerAttributeManager. 

86 table = context.addTable(cls._TABLE_NAME, cls._TABLE_SPEC) 

87 return cls(db=db, table=table) 

88 

89 def _checkTableExists(self) -> None: 

90 """Check that attributes table exists or raise an exception. 

91 

92 This should not be called on every read operation but instead only 

93 when an exception is raised from SELECT query. 

94 """ 

95 # Database metadata is likely not populated at this point yet, so we 

96 # have to use low-level connection object to check it. 

97 # TODO: Once we have stable gen3 schema everywhere this test can be 

98 # dropped (DM-27373). 

99 if not self._table.exists(bind=self._db._connection): 

100 raise MissingAttributesTableError( 

101 f"`{self._table.name}` table is missing from schema, schema has to" 

102 " be initialized before use (database is probably outdated)." 

103 ) from None 

104 

105 def get(self, name: str, default: Optional[str] = None) -> Optional[str]: 

106 # Docstring inherited from ButlerAttributeManager. 

107 try: 

108 sql = sqlalchemy.sql.select([self._table.columns.value]).where( 

109 self._table.columns.name == name 

110 ) 

111 row = self._db.query(sql).fetchone() 

112 if row is not None: 

113 return row[0] 

114 return default 

115 except sqlalchemy.exc.OperationalError: 

116 # if this is due to missing table raise different exception 

117 self._checkTableExists() 

118 raise 

119 

120 def set(self, name: str, value: str, *, force: bool = False) -> None: 

121 # Docstring inherited from ButlerAttributeManager. 

122 if not name or not value: 

123 raise ValueError("name and value cannot be empty") 

124 if force: 

125 self._db.replace(self._table, { 

126 "name": name, 

127 "value": value, 

128 }) 

129 else: 

130 try: 

131 self._db.insert(self._table, { 

132 "name": name, 

133 "value": value, 

134 }) 

135 except sqlalchemy.exc.IntegrityError as exc: 

136 raise ButlerAttributeExistsError(f"attribute {name} already exists") from exc 

137 

138 def delete(self, name: str) -> bool: 

139 # Docstring inherited from ButlerAttributeManager. 

140 numRows = self._db.delete(self._table, ["name"], {"name": name}) 

141 return numRows > 0 

142 

143 def items(self) -> Iterable[Tuple[str, str]]: 

144 # Docstring inherited from ButlerAttributeManager. 

145 sql = sqlalchemy.sql.select([ 

146 self._table.columns.name, 

147 self._table.columns.value, 

148 ]) 

149 for row in self._db.query(sql): 

150 yield row[0], row[1] 

151 

152 def empty(self) -> bool: 

153 # Docstring inherited from ButlerAttributeManager. 

154 try: 

155 sql = sqlalchemy.sql.select([sqlalchemy.sql.func.count()]).select_from(self._table) 

156 row = self._db.query(sql).fetchone() 

157 return row[0] == 0 

158 except sqlalchemy.exc.OperationalError: 

159 # if this is due to missing table raise different exception 

160 self._checkTableExists() 

161 raise 

162 

163 @classmethod 

164 def currentVersion(cls) -> Optional[VersionTuple]: 

165 # Docstring inherited from VersionedExtension. 

166 return _VERSION 

167 

168 def schemaDigest(self) -> Optional[str]: 

169 # Docstring inherited from VersionedExtension. 

170 return self._defaultSchemaDigest([self._table], self._db.dialect)