Coverage for filip/utils/simple_ql.py: 72%

111 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-11-20 16:54 +0000

1""" 

2The Simple Query Language provides a simplified syntax to retrieve entities 

3which match a set of conditions. A query is composed by a list of 

4statements separated by the ';' character. Each statement expresses a 

5matching condition. The query returns all the entities that match all 

6the matching conditions (AND logical operator). 

7 

8For further details of the language please refer to: 

9 

10https://telefonicaid.github.io/fiware-orion/api/v2/stable/ 

11""" 

12import regex as re 

13from aenum import Enum 

14from typing import Union, List, Tuple, Any 

15 

16 

17class Operator(str, Enum): 

18 """ 

19 The list of operators (and the format of the values they use) is as follows: 

20 """ 

21 _init_ = 'value __doc__' 

22 

23 EQUAL = '==', "Single element, e.g. temperature!=41. For an entity to " \ 

24 "match, it must contain the target property (temperature) " \ 

25 "and the target property value must not be the query value " \ 

26 "(41). " \ 

27 "A list of comma-separated values, e.g. color!=black," \ 

28 "red. For an entity to match, it must contain the target " \ 

29 "property and the target property value must not be any " \ 

30 "of the values in the list (AND clause) (or not include any "\ 

31 "of the values in the list in case the target property " \ 

32 "value is an array). Eg. entities whose attribute color is " \ 

33 "set to black will not match, while entities whose " \ 

34 "attribute color is set to white will match." \ 

35 "A range, specified as a minimum and maximum separated by " \ 

36 ".., e.g. temperature!=10..20. For an entity to match, " \ 

37 "it must contain the target property (temperature) and the " \ 

38 "target property value must not be between the upper and " \ 

39 "lower limits (both included). Ranges can only be used " \ 

40 "with elements target properties that represent dates (in " \ 

41 "ISO8601 format), numbers or strings. " 

42 UNEQUAL = '!=', "Single element, e.g. temperature!=41. For an entity to " \ 

43 "match, it must contain the target property " \ 

44 "(temperature) and the target property value must not be " \ 

45 "the query value (41). A list of comma-separated values, " \ 

46 "e.g. color!=black,red. For an entity to match, it must " \ 

47 "contain the target property and the target property " \ 

48 "value must not be any of the values in the list (AND " \ 

49 "clause) (or not include any of the values in the list " \ 

50 "in case the target property value is an array). Eg. " \ 

51 "entities whose attribute color is set to black will not " \ 

52 "match, while entities whose attribute color is set to " \ 

53 "white will match. A range, specified as a minimum and " \ 

54 "maximum separated by .., e.g. temperature!=10..20. For " \ 

55 "an entity to match, it must contain the target property " \ 

56 "(temperature) and the target property value must not be " \ 

57 "between the upper and lower limits (both included). " \ 

58 "Ranges can only be used with elements target properties " \ 

59 "that represent dates (in ISO8601 format), numbers or " \ 

60 "strings. " 

61 GREATER_THAN = '>', "The right-hand side must be a single element, e.g. " \ 

62 "temperature>42. For an entity to match, it must " \ 

63 "contain the target property (temperature) and the " \ 

64 "target property value must be strictly greater than " \ 

65 "the query value (42). This operation is only valid " \ 

66 "for target properties of type date, number or " \ 

67 "string (used with target properties of other types " \ 

68 "may lead to unpredictable results). " 

69 LESS_THAN = '<', "The right-hand side must be a single element, e.g. " \ 

70 "temperature<43. For an entity to match, it must " \ 

71 "contain the target property (temperature) and the " \ 

72 "target property value must be strictly less than the " \ 

73 "value (43). This operation is only valid for target " \ 

74 "properties of type date, number or string (used with " \ 

75 "target properties of other types may lead to " \ 

76 "unpredictable results). " 

77 GREATER_OR_EQUAL = '>=', "The right-hand side must be a single element, " \ 

78 "e.g. temperature>=44. For an entity to match, " \ 

79 "it must contain the target property (" \ 

80 "temperature) and the target property value " \ 

81 "must be greater than or equal to that value " \ 

82 "(44). This operation is only valid for target " \ 

83 "properties of type date, number or string " \ 

84 "(used with target properties of other types " \ 

85 "may lead to unpredictable results). " 

86 LESS_OR_EQUAL = '<=', "The right-hand side must be a single element, " \ 

87 "e.g. temperature<=45. For an entity to match, " \ 

88 "it must contain the target property (temperature) " \ 

89 "and the target property value must be less than " \ 

90 "or equal to that value (45). This operation is " \ 

91 "only valid for target properties of type date, " \ 

92 "number or string (used with target properties of " \ 

93 "other types may lead to unpredictable results). " 

94 MATCH_PATTERN = '~=', "The value matches a given pattern, expressed as a " \ 

95 "regular expression, e.g. color~=ow. For an entity " \ 

96 "to match, it must contain the target property (" \ 

97 "color) and the target property value must match " \ 

98 "the string in the right-hand side, 'ow' in this " \ 

99 "example (brown and yellow would match, black and " \ 

100 "white would not). This operation is only valid " \ 

101 "for target properties of type string. " 

102 

103 @classmethod 

104 def list(cls): 

105 """ 

106 

107 Returns: 

108 list of all valid values 

109 """ 

110 return list(map(lambda c: c.value, cls)) 

111 

112 

113class QueryStatement(Tuple): 

114 """ 

115 Simple query statement 

116 """ 

117 

118 def __new__(cls, left: str, op: Union[str, Operator], right: Any): 

119 q = tuple.__new__(QueryStatement, (left, op, right)) 

120 q = cls.validate(q) 

121 return q 

122 

123 @classmethod 

124 def __get_validators__(cls): 

125 yield cls.validate 

126 

127 @classmethod 

128 def validate(cls, value): 

129 """ 

130 Validates statements 

131 

132 Args: 

133 value: 

134 

135 Returns: 

136 """ 

137 if isinstance(value, (tuple, QueryStatement)): 

138 if len(value) != 3: 

139 raise TypeError('3-tuple required') 

140 if not isinstance(value[0], str): 

141 raise TypeError('First argument must be a string!') 

142 if value[1] not in Operator.list(): 

143 raise TypeError('Invalid comparison operator!') 

144 if value[1] not in [Operator.EQUAL, 

145 Operator.UNEQUAL, 

146 Operator.MATCH_PATTERN]: 

147 try: 

148 float(value[2]) 

149 except ValueError as err: 

150 err.args += ("Invalid combination of operator and right " 

151 "hand side!",) 

152 raise 

153 return value 

154 elif isinstance(value, str): 

155 return cls.parse_str(value) 

156 raise TypeError 

157 

158 def to_str(self): 

159 """ 

160 Parses QueryStatement to String 

161 

162 Returns: 

163 String 

164 """ 

165 if not isinstance(self[2], str): 

166 right = str(self[2]) 

167 elif self[2].isnumeric(): 

168 right = f"{self[2]}" 

169 else: 

170 right = self[2] 

171 return ''.join([self[0], self[1], right]) 

172 

173 @classmethod 

174 def parse_str(cls, string: str): 

175 """ 

176 Generates QueryStatement form string 

177 

178 Args: 

179 string: 

180 

181 Returns: 

182 QueryStatement 

183 """ 

184 for op in Operator.list(): 

185 if re.fullmatch(rf"^\w((\w|[^&,?,/,#,\*,\s]\w)?)*{op}\w+$", string): 

186 args = string.split(op) 

187 if len(args) == 2: 

188 if args[1].isnumeric(): 

189 try: 

190 right = int(args[1]) 

191 except ValueError: 

192 right = float(args[1]) 

193 return QueryStatement(args[0], op, right) 

194 return QueryStatement(args[0], op, args[1]) 

195 raise ValueError 

196 

197 def __str__(self): 

198 """ Return str(self). """ 

199 return self.to_str() 

200 

201 def __repr__(self): 

202 """ Return repr(self). """ 

203 return self.to_str().__repr__() 

204 

205 

206class QueryString: 

207 """ 

208 Class for validated QueryStrings that can be used in api clients 

209 """ 

210 def __init__(self, qs: Union[Tuple, 

211 QueryStatement, 

212 List[Union[QueryStatement, Tuple]]]): 

213 qs = self.__check_arguments(qs=qs) 

214 self._qs = qs 

215 

216 @classmethod 

217 def __check_arguments(cls, qs): 

218 """ 

219 Check arguments on consistency 

220 

221 Args: 

222 qs: queny statement object 

223 

224 returns: 

225 List of QueryStatements 

226 """ 

227 if isinstance(qs, List): 

228 for idx, item in enumerate(qs): 

229 if not isinstance(item, QueryStatement): 

230 qs[idx] = QueryStatement(*item) 

231 # Remove duplicates 

232 qs = list(dict.fromkeys(qs)) 

233 elif isinstance(qs, QueryStatement): 

234 qs = [qs] 

235 elif isinstance(qs, tuple): 

236 qs = [QueryStatement(*qs)] 

237 else: 

238 raise ValueError('Invalid argument!') 

239 return qs 

240 

241 def update(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]): 

242 """ 

243 Adds or updates QueryStatement within QueryString. First to arguments 

244 must match an existing argument for update. This redundant rules 

245 

246 Args: 

247 qs: Query statement to add to the string object 

248 

249 Returns: 

250 None 

251 """ 

252 qs = self.__check_arguments(qs=qs) 

253 self._qs.extend(qs) 

254 self._qs = list(dict.fromkeys(qs)) 

255 

256 def remove(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]): 

257 """ 

258 Remove Statement from QueryString 

259 Args: 

260 qs: 

261 

262 Returns: 

263 

264 """ 

265 qs = self.__check_arguments(qs=qs) 

266 for q in qs: 

267 self._qs.remove(q) 

268 

269 @classmethod 

270 def __get_validators__(cls): 

271 yield cls.validate 

272 

273 @classmethod 

274 def validate(cls, v): 

275 """validate QueryString""" 

276 if isinstance(v, QueryString): 

277 return v 

278 if isinstance(v, str): 

279 return cls.parse_str(v) 

280 raise ValueError('Invalid argument!') 

281 

282 def to_str(self): 

283 """ 

284 Parsing self.qs to string object 

285 

286 Returns: 

287 String: query string that can be added to requests as parameter 

288 """ 

289 return ';'.join([q.to_str() for q in self._qs]) 

290 

291 @classmethod 

292 def parse_str(cls, string: str): 

293 """ 

294 Creates QueryString from string 

295 

296 Args: 

297 string: 

298 

299 Returns: 

300 QueryString 

301 """ 

302 q_parts = string.split(';') 

303 qs = [] 

304 for part in q_parts: 

305 q = QueryStatement.parse_str(part) 

306 qs.append(q) 

307 return QueryString(qs=qs) 

308 

309 def __str__(self): 

310 """ Return str(self). """ 

311 return self.to_str() 

312 

313 def __repr__(self): 

314 """ Return repr(self). """ 

315 return self.to_str().__repr__()