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

109 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-02 08:01 +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 return value 

168 elif isinstance(value, str): 

169 return cls.parse_str(value) 

170 raise TypeError 

171 

172 def to_str(self): 

173 """ 

174 Parses QueryStatement to String 

175 

176 Returns: 

177 String 

178 """ 

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

180 right = str(self[2]) 

181 elif self[2].isnumeric(): 

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

183 else: 

184 right = self[2] 

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

186 

187 @classmethod 

188 def parse_str(cls, string: str): 

189 """ 

190 Generates QueryStatement form string 

191 

192 Args: 

193 string: 

194 

195 Returns: 

196 QueryStatement 

197 """ 

198 for op in Operator.list(): 

199 if re.fullmatch( 

200 rf"^\w(?:(?:\w|[^&,?,/,#,\*,\s]\w)?)*{op}[\w.,:'-]+$", string 

201 ): 

202 args = string.split(op) 

203 if len(args) == 2: 

204 if args[1].isnumeric(): 

205 try: 

206 right = int(args[1]) 

207 except ValueError: 

208 right = float(args[1]) 

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

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

211 raise ValueError 

212 raise ValueError("Invalid query statement string!") 

213 

214 def __str__(self): 

215 """Return str(self).""" 

216 return self.to_str() 

217 

218 def __repr__(self): 

219 """Return repr(self).""" 

220 return self.to_str().__repr__() 

221 

222 

223class QueryString: 

224 """ 

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

226 """ 

227 

228 def __init__( 

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

230 ): 

231 qs = self.__check_arguments(qs=qs) 

232 self._qs = qs 

233 

234 @classmethod 

235 def __check_arguments(cls, qs): 

236 """ 

237 Check arguments on consistency 

238 

239 Args: 

240 qs: queny statement object 

241 

242 returns: 

243 List of QueryStatements 

244 """ 

245 if isinstance(qs, List): 

246 for idx, item in enumerate(qs): 

247 if not isinstance(item, QueryStatement): 

248 qs[idx] = QueryStatement(*item) 

249 # Remove duplicates 

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

251 elif isinstance(qs, QueryStatement): 

252 qs = [qs] 

253 elif isinstance(qs, tuple): 

254 qs = [QueryStatement(*qs)] 

255 else: 

256 raise ValueError("Invalid argument!") 

257 return qs 

258 

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

260 """ 

261 Adds or updates QueryStatement within QueryString. First to arguments 

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

263 

264 Args: 

265 qs: Query statement to add to the string object 

266 

267 Returns: 

268 None 

269 """ 

270 qs = self.__check_arguments(qs=qs) 

271 self._qs.extend(qs) 

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

273 

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

275 """ 

276 Remove Statement from QueryString 

277 Args: 

278 qs: 

279 

280 Returns: 

281 

282 """ 

283 qs = self.__check_arguments(qs=qs) 

284 for q in qs: 

285 self._qs.remove(q) 

286 

287 @classmethod 

288 def __get_validators__(cls): 

289 yield cls.validate 

290 

291 @classmethod 

292 def validate(cls, v): 

293 """validate QueryString""" 

294 if isinstance(v, QueryString): 

295 return v 

296 if isinstance(v, str): 

297 return cls.parse_str(v) 

298 raise ValueError("Invalid argument!") 

299 

300 def to_str(self): 

301 """ 

302 Parsing self.qs to string object 

303 

304 Returns: 

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

306 """ 

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

308 

309 @classmethod 

310 def parse_str(cls, string: str): 

311 """ 

312 Creates QueryString from string 

313 

314 Args: 

315 string: 

316 

317 Returns: 

318 QueryString 

319 """ 

320 q_parts = string.split(";") 

321 qs = [] 

322 for part in q_parts: 

323 q = QueryStatement.parse_str(part) 

324 qs.append(q) 

325 return QueryString(qs=qs) 

326 

327 def __str__(self): 

328 """Return str(self).""" 

329 return self.to_str() 

330 

331 def __repr__(self): 

332 """Return repr(self).""" 

333 return self.to_str().__repr__()