Coverage for python/lsst/daf/butler/registry/interfaces/_versioning.py : 26%

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/>.
22from __future__ import annotations
24__all__ = [
25 "VersionTuple",
26 "VersionedExtension",
27]
29from abc import ABC, abstractmethod
30import hashlib
31from typing import (
32 Iterable,
33 NamedTuple,
34 Optional,
35)
37import sqlalchemy
40class VersionTuple(NamedTuple):
41 """Class representing a version number.
43 Parameters
44 ----------
45 major, minor, patch : `int`
46 Version number components
47 """
48 major: int
49 minor: int
50 patch: int
52 @classmethod
53 def fromString(cls, versionStr: str) -> VersionTuple:
54 """Extract version number from a string.
56 Parameters
57 ----------
58 versionStr : `str`
59 Version number in string form "X.Y.Z", all components must be
60 present.
62 Returns
63 -------
64 version : `VersionTuple`
65 Parsed version tuple.
67 Raises
68 ------
69 ValueError
70 Raised if string has an invalid format.
71 """
72 try:
73 version = tuple(int(v) for v in versionStr.split("."))
74 except ValueError as exc:
75 raise ValueError(f"Invalid version string '{versionStr}'") from exc
76 if len(version) != 3:
77 raise ValueError(f"Invalid version string '{versionStr}', must consist of three numbers")
78 return cls(*version)
80 def __str__(self) -> str:
81 """Transform version tuple into a canonical string form.
82 """
83 return f"{self.major}.{self.minor}.{self.patch}"
86class VersionedExtension(ABC):
87 """Interface for extension classes with versions.
88 """
90 @classmethod
91 @abstractmethod
92 def currentVersion(cls) -> Optional[VersionTuple]:
93 """Return extension version as defined by current implementation.
95 This method can return ``None`` if an extension does not require
96 its version to be saved or checked.
98 Returns
99 -------
100 version : `VersionTuple`
101 Current extension version or ``None``.
102 """
103 raise NotImplementedError()
105 @classmethod
106 def extensionName(cls) -> str:
107 """Return full name of the extension.
109 This name should match the name defined in registry configuration. It
110 is also stored in registry attributes. Default implementation returns
111 full class name.
113 Returns
114 -------
115 name : `str`
116 Full extension name.
117 """
118 return f"{cls.__module__}.{cls.__name__}"
120 @abstractmethod
121 def schemaDigest(self) -> Optional[str]:
122 """Return digest for schema piece managed by this extension.
124 Returns
125 -------
126 digest : `str` or `None`
127 String representation of the digest of the schema, ``None`` should
128 be returned if schema digest is not to be saved or checked. The
129 length of the returned string cannot exceed the length of the
130 "value" column of butler attributes table, currently 65535
131 characters.
133 Notes
134 -----
135 There is no exact definition of digest format, any string should work.
136 The only requirement for string contents is that it has to remain
137 stable over time if schema does not change but it should produce
138 different string for any change in the schema. In many cases default
139 implementation in `_defaultSchemaDigest` can be used as a reasonable
140 choice.
141 """
142 raise NotImplementedError()
144 def _defaultSchemaDigest(self, tables: Iterable[sqlalchemy.schema.Table]) -> str:
145 """Calculate digest for a schema based on list of tables schemas.
147 Parameters
148 ----------
149 tables : iterable [`sqlalchemy.schema.Table`]
150 Set of tables comprising the schema.
152 Returns
153 -------
154 digest : `str`
155 String representation of the digest of the schema.
157 Notes
158 -----
159 It is not specified what kind of implementation is used to calculate
160 digest string. The only requirement for that is that result should be
161 stable over time as this digest string will be stored in the database.
162 It should detect (by producing different digests) sensible changes to
163 the schema, but it also should be stable w.r.t. changes that do
164 not actually change the schema (e.g. change in the order of columns or
165 keys.) Current implementation is likely incomplete in that it does not
166 detect all possible changes (e.g. some constraints may not be included
167 into total digest).
168 """
170 def tableSchemaRepr(table: sqlalchemy.schema.Table) -> str:
171 """Make string representation of a single table schema.
172 """
173 tableSchemaRepr = [table.name]
174 schemaReps = []
175 for column in table.columns:
176 columnRep = f"COL,{column.name},{column.type}"
177 if column.primary_key:
178 columnRep += ",PK"
179 if column.nullable:
180 columnRep += ",NULL"
181 schemaReps += [columnRep]
182 for fkConstr in table.foreign_key_constraints:
183 # for foreign key we include only one side of relations into
184 # digest, other side could be managed by different extension
185 fkReps = ["FK", fkConstr.name] + [fk.column.name for fk in fkConstr.elements]
186 fkRep = ",".join(fkReps)
187 schemaReps += [fkRep]
188 # sort everything to keep it stable
189 schemaReps.sort()
190 tableSchemaRepr += schemaReps
191 return ";".join(tableSchemaRepr)
193 md5 = hashlib.md5()
194 tableSchemas = sorted(tableSchemaRepr(table) for table in tables)
195 for tableRepr in tableSchemas:
196 md5.update(tableRepr.encode())
197 digest = md5.hexdigest()
198 return digest