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

111 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-02-19 11:48 +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""" 

12 

13import regex as re 

14from aenum import Enum 

15from typing import Union, List, Tuple, Any 

16 

17 

18class Operator(str, Enum): 

19 """ 

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

21 """ 

22 

23 _init_ = "value __doc__" 

24 

25 EQUAL = ( 

26 "==", 

27 "Single element, e.g. temperature!=41. For an entity to " 

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

29 "and the target property value must not be the query value " 

30 "(41). " 

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

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

33 "property and the target property value must not be any " 

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

35 "of the values in the list in case the target property " 

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

37 "set to black will not match, while entities whose " 

38 "attribute color is set to white will match." 

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

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

41 "it must contain the target property (temperature) and the " 

42 "target property value must not be between the upper and " 

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

44 "with elements target properties that represent dates (in " 

45 "ISO8601 format), numbers or strings. ", 

46 ) 

47 UNEQUAL = ( 

48 "!=", 

49 "Single element, e.g. temperature!=41. For an entity to " 

50 "match, it must contain the target property " 

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

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

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

54 "contain the target property and the target property " 

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

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

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

58 "entities whose attribute color is set to black will not " 

59 "match, while entities whose attribute color is set to " 

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

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

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

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

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

65 "Ranges can only be used with elements target properties " 

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

67 "strings. ", 

68 ) 

69 GREATER_THAN = ( 

70 ">", 

71 "The right-hand side must be a single element, e.g. " 

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

73 "contain the target property (temperature) and the " 

74 "target property value must be strictly greater than " 

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

76 "for target properties of type date, number or " 

77 "string (used with target properties of other types " 

78 "may lead to unpredictable results). ", 

79 ) 

80 LESS_THAN = ( 

81 "<", 

82 "The right-hand side must be a single element, e.g. " 

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

84 "contain the target property (temperature) and the " 

85 "target property value must be strictly less than the " 

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

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

88 "target properties of other types may lead to " 

89 "unpredictable results). ", 

90 ) 

91 GREATER_OR_EQUAL = ( 

92 ">=", 

93 "The right-hand side must be a single element, " 

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

95 "it must contain the target property (" 

96 "temperature) and the target property value " 

97 "must be greater than or equal to that value " 

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

99 "properties of type date, number or string " 

100 "(used with target properties of other types " 

101 "may lead to unpredictable results). ", 

102 ) 

103 LESS_OR_EQUAL = ( 

104 "<=", 

105 "The right-hand side must be a single element, " 

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

107 "it must contain the target property (temperature) " 

108 "and the target property value must be less than " 

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

110 "only valid for target properties of type date, " 

111 "number or string (used with target properties of " 

112 "other types may lead to unpredictable results). ", 

113 ) 

114 MATCH_PATTERN = ( 

115 "~=", 

116 "The value matches a given pattern, expressed as a " 

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

118 "to match, it must contain the target property (" 

119 "color) and the target property value must match " 

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

121 "example (brown and yellow would match, black and " 

122 "white would not). This operation is only valid " 

123 "for target properties of type string. ", 

124 ) 

125 

126 @classmethod 

127 def list(cls): 

128 """ 

129 

130 Returns: 

131 list of all valid values 

132 """ 

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

134 

135 

136class QueryStatement(Tuple): 

137 """ 

138 Simple query statement 

139 """ 

140 

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

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

143 q = cls.validate(q) 

144 return q 

145 

146 @classmethod 

147 def __get_validators__(cls): 

148 yield cls.validate 

149 

150 @classmethod 

151 def validate(cls, value): 

152 """ 

153 Validates statements 

154 

155 Args: 

156 value: 

157 

158 Returns: 

159 """ 

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

161 if len(value) != 3: 

162 raise TypeError("3-tuple required") 

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

164 raise TypeError("First argument must be a string!") 

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

166 raise TypeError("Invalid comparison operator!") 

167 if value[1] not in [ 

168 Operator.EQUAL, 

169 Operator.UNEQUAL, 

170 Operator.MATCH_PATTERN, 

171 ]: 

172 try: 

173 float(value[2]) 

174 except ValueError as err: 

175 err.args += ( 

176 "Invalid combination of operator and right " "hand side!", 

177 ) 

178 raise 

179 return value 

180 elif isinstance(value, str): 

181 return cls.parse_str(value) 

182 raise TypeError 

183 

184 def to_str(self): 

185 """ 

186 Parses QueryStatement to String 

187 

188 Returns: 

189 String 

190 """ 

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

192 right = str(self[2]) 

193 elif self[2].isnumeric(): 

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

195 else: 

196 right = self[2] 

197 return "".join([self[0], self[1], right]) 

198 

199 @classmethod 

200 def parse_str(cls, string: str): 

201 """ 

202 Generates QueryStatement form string 

203 

204 Args: 

205 string: 

206 

207 Returns: 

208 QueryStatement 

209 """ 

210 for op in Operator.list(): 

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

212 args = string.split(op) 

213 if len(args) == 2: 

214 if args[1].isnumeric(): 

215 try: 

216 right = int(args[1]) 

217 except ValueError: 

218 right = float(args[1]) 

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

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

221 raise ValueError 

222 

223 def __str__(self): 

224 """Return str(self).""" 

225 return self.to_str() 

226 

227 def __repr__(self): 

228 """Return repr(self).""" 

229 return self.to_str().__repr__() 

230 

231 

232class QueryString: 

233 """ 

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

235 """ 

236 

237 def __init__( 

238 self, qs: Union[Tuple, QueryStatement, List[Union[QueryStatement, Tuple]]] 

239 ): 

240 qs = self.__check_arguments(qs=qs) 

241 self._qs = qs 

242 

243 @classmethod 

244 def __check_arguments(cls, qs): 

245 """ 

246 Check arguments on consistency 

247 

248 Args: 

249 qs: queny statement object 

250 

251 returns: 

252 List of QueryStatements 

253 """ 

254 if isinstance(qs, List): 

255 for idx, item in enumerate(qs): 

256 if not isinstance(item, QueryStatement): 

257 qs[idx] = QueryStatement(*item) 

258 # Remove duplicates 

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

260 elif isinstance(qs, QueryStatement): 

261 qs = [qs] 

262 elif isinstance(qs, tuple): 

263 qs = [QueryStatement(*qs)] 

264 else: 

265 raise ValueError("Invalid argument!") 

266 return qs 

267 

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

269 """ 

270 Adds or updates QueryStatement within QueryString. First to arguments 

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

272 

273 Args: 

274 qs: Query statement to add to the string object 

275 

276 Returns: 

277 None 

278 """ 

279 qs = self.__check_arguments(qs=qs) 

280 self._qs.extend(qs) 

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

282 

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

284 """ 

285 Remove Statement from QueryString 

286 Args: 

287 qs: 

288 

289 Returns: 

290 

291 """ 

292 qs = self.__check_arguments(qs=qs) 

293 for q in qs: 

294 self._qs.remove(q) 

295 

296 @classmethod 

297 def __get_validators__(cls): 

298 yield cls.validate 

299 

300 @classmethod 

301 def validate(cls, v): 

302 """validate QueryString""" 

303 if isinstance(v, QueryString): 

304 return v 

305 if isinstance(v, str): 

306 return cls.parse_str(v) 

307 raise ValueError("Invalid argument!") 

308 

309 def to_str(self): 

310 """ 

311 Parsing self.qs to string object 

312 

313 Returns: 

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

315 """ 

316 return ";".join([q.to_str() for q in self._qs]) 

317 

318 @classmethod 

319 def parse_str(cls, string: str): 

320 """ 

321 Creates QueryString from string 

322 

323 Args: 

324 string: 

325 

326 Returns: 

327 QueryString 

328 """ 

329 q_parts = string.split(";") 

330 qs = [] 

331 for part in q_parts: 

332 q = QueryStatement.parse_str(part) 

333 qs.append(q) 

334 return QueryString(qs=qs) 

335 

336 def __str__(self): 

337 """Return str(self).""" 

338 return self.to_str() 

339 

340 def __repr__(self): 

341 """Return repr(self).""" 

342 return self.to_str().__repr__()