Coverage for python/lsst/daf/butler/core/constraints.py: 26%

52 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-08-05 01:26 +0000

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/>. 

21 

22"""Code relating to constraints based on `DatasetRef`, `DatasetType`, or 

23`StorageClass`. 

24""" 

25 

26from __future__ import annotations 

27 

28__all__ = ("Constraints", "ConstraintsValidationError", "ConstraintsConfig") 

29 

30import logging 

31from typing import TYPE_CHECKING 

32 

33from .config import Config 

34from .configSupport import LookupKey, processLookupConfigList 

35from .exceptions import ValidationError 

36 

37if TYPE_CHECKING: 

38 from .datasets import DatasetRef, DatasetType 

39 from .dimensions import DimensionUniverse 

40 from .storageClass import StorageClass 

41 

42log = logging.getLogger(__name__) 

43 

44 

45class ConstraintsValidationError(ValidationError): 

46 """Thrown when a constraints list has mutually exclusive definitions.""" 

47 

48 pass 

49 

50 

51class ConstraintsConfig(Config): 

52 """Configuration information for `Constraints`.""" 

53 

54 pass 

55 

56 

57class Constraints: 

58 """Determine whether an entity is allowed to be handled. 

59 

60 Supported entities are `DatasetRef`, `DatasetType`, or `StorageClass`. 

61 

62 Parameters 

63 ---------- 

64 config : `ConstraintsConfig` or `str` 

65 Load configuration. If `None` then this is equivalent to having 

66 no restrictions. 

67 universe : `DimensionUniverse` 

68 The set of all known dimensions, used to normalize any lookup keys 

69 involving dimensions. 

70 """ 

71 

72 matchAllKey = LookupKey("all") 

73 """Configuration key associated with matching everything.""" 

74 

75 def __init__(self, config: ConstraintsConfig | str | None, *, universe: DimensionUniverse): 

76 # Default is to accept all and reject nothing 

77 self._accept = set() 

78 self._reject = set() 

79 

80 if config is not None: 

81 self.config = ConstraintsConfig(config) 

82 

83 if "accept" in self.config: 

84 self._accept = processLookupConfigList(self.config["accept"], universe=universe) 

85 if "reject" in self.config: 

86 self._reject = processLookupConfigList(self.config["reject"], universe=universe) 

87 

88 if self.matchAllKey in self._accept and self.matchAllKey in self._reject: 

89 raise ConstraintsValidationError( 

90 "Can not explicitly accept 'all' and reject 'all' in one configuration" 

91 ) 

92 

93 def __str__(self) -> str: 

94 # Standard stringification 

95 if not self._accept and not self._reject: 

96 return "Accepts: all" 

97 

98 accepts = ", ".join(str(k) for k in self._accept) 

99 rejects = ", ".join(str(k) for k in self._reject) 

100 return f"Accepts: {accepts}; Rejects: {rejects}" 

101 

102 def isAcceptable(self, entity: DatasetRef | DatasetType | StorageClass) -> bool: 

103 """Check whether the supplied entity will be acceptable. 

104 

105 Parameters 

106 ---------- 

107 entity : `DatasetType`, `DatasetRef`, or `StorageClass` 

108 Instance to use to look in constraints table. 

109 The entity itself reports the `LookupKey` that is relevant. 

110 

111 Returns 

112 ------- 

113 allowed : `bool` 

114 `True` if the entity is allowed. 

115 """ 

116 # Get the names to use for lookup 

117 names = set(entity._lookupNames()) 

118 

119 # Test if this entity is explicitly mentioned for accept/reject 

120 isExplicitlyAccepted = bool(names & self._accept) 

121 

122 if isExplicitlyAccepted: 

123 return True 

124 

125 isExplicitlyRejected = bool(names & self._reject) 

126 

127 if isExplicitlyRejected: 

128 return False 

129 

130 # Now look for wildcard match -- we have to also check for dataId 

131 # overrides 

132 

133 # Generate a new set of lookup keys that use the wildcard name 

134 # but the supplied dimensions 

135 wildcards = {k.clone(name=self.matchAllKey.name) for k in names} 

136 

137 isWildcardAccepted = bool(wildcards & self._accept) 

138 isWildcardRejected = bool(wildcards & self._reject) 

139 

140 if isWildcardRejected: 

141 return False 

142 

143 # If all the wildcard and explicit rejections have failed then 

144 # if the accept list is empty, or if a wildcard acceptance worked 

145 # we can accept, else reject 

146 if isWildcardAccepted or not self._accept: 

147 return True 

148 

149 return False 

150 

151 def getLookupKeys(self) -> set[LookupKey]: 

152 """Retrieve the look up keys for all the constraints entries. 

153 

154 Returns 

155 ------- 

156 keys : `set` of `LookupKey` 

157 The keys available for determining constraints. Does not include 

158 the special "all" lookup key. 

159 """ 

160 all = self._accept | self._accept 

161 return {a for a in all if a.name != self.matchAllKey.name}