Coverage for python/lsst/daf/butler/registry/attributes.py : 32%

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"""
26__all__ = ["DefaultButlerAttributeManager"]
28from typing import (
29 ClassVar,
30 Iterable,
31 Optional,
32 Tuple,
33)
35import sqlalchemy
37from ..core.ddl import TableSpec, FieldSpec
38from .interfaces import (
39 Database,
40 ButlerAttributeExistsError,
41 ButlerAttributeManager,
42 StaticTablesContext,
43 VersionTuple
44)
47class MissingAttributesTableError(RuntimeError):
48 """Exception raised when a database is missing attributes table.
49 """
50 pass
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)
59class DefaultButlerAttributeManager(ButlerAttributeManager):
60 """An implementation of `ButlerAttributeManager` that stores attributes
61 in a database table.
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
74 _TABLE_NAME: ClassVar[str] = "butler_attributes"
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 )
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)
89 def _checkTableExists(self) -> None:
90 """Check that attributes table exists or raise an exception.
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
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
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
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
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]
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
163 @classmethod
164 def currentVersion(cls) -> Optional[VersionTuple]:
165 # Docstring inherited from VersionedExtension.
166 return _VERSION
168 def schemaDigest(self) -> Optional[str]:
169 # Docstring inherited from VersionedExtension.
170 return self._defaultSchemaDigest([self._table], self._db.dialect)