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
« 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).
8For further details of the language please refer to:
10https://telefonicaid.github.io/fiware-orion/api/v2/stable/
11"""
13import regex as re
14from aenum import Enum
15from typing import Union, List, Tuple, Any
18class Operator(str, Enum):
19 """
20 The list of operators (and the format of the values they use) is as follows:
21 """
23 _init_ = "value __doc__"
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 )
126 @classmethod
127 def list(cls):
128 """
130 Returns:
131 list of all valid values
132 """
133 return list(map(lambda c: c.value, cls))
136class QueryStatement(Tuple):
137 """
138 Simple query statement
139 """
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
146 @classmethod
147 def __get_validators__(cls):
148 yield cls.validate
150 @classmethod
151 def validate(cls, value):
152 """
153 Validates statements
155 Args:
156 value:
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
184 def to_str(self):
185 """
186 Parses QueryStatement to String
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])
199 @classmethod
200 def parse_str(cls, string: str):
201 """
202 Generates QueryStatement form string
204 Args:
205 string:
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
223 def __str__(self):
224 """Return str(self)."""
225 return self.to_str()
227 def __repr__(self):
228 """Return repr(self)."""
229 return self.to_str().__repr__()
232class QueryString:
233 """
234 Class for validated QueryStrings that can be used in api clients
235 """
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
243 @classmethod
244 def __check_arguments(cls, qs):
245 """
246 Check arguments on consistency
248 Args:
249 qs: queny statement object
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
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
273 Args:
274 qs: Query statement to add to the string object
276 Returns:
277 None
278 """
279 qs = self.__check_arguments(qs=qs)
280 self._qs.extend(qs)
281 self._qs = list(dict.fromkeys(qs))
283 def remove(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]):
284 """
285 Remove Statement from QueryString
286 Args:
287 qs:
289 Returns:
291 """
292 qs = self.__check_arguments(qs=qs)
293 for q in qs:
294 self._qs.remove(q)
296 @classmethod
297 def __get_validators__(cls):
298 yield cls.validate
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!")
309 def to_str(self):
310 """
311 Parsing self.qs to string object
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])
318 @classmethod
319 def parse_str(cls, string: str):
320 """
321 Creates QueryString from string
323 Args:
324 string:
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)
336 def __str__(self):
337 """Return str(self)."""
338 return self.to_str()
340 def __repr__(self):
341 """Return repr(self)."""
342 return self.to_str().__repr__()