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
« 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).
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 return value
168 elif isinstance(value, str):
169 return cls.parse_str(value)
170 raise TypeError
172 def to_str(self):
173 """
174 Parses QueryStatement to String
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])
187 @classmethod
188 def parse_str(cls, string: str):
189 """
190 Generates QueryStatement form string
192 Args:
193 string:
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!")
214 def __str__(self):
215 """Return str(self)."""
216 return self.to_str()
218 def __repr__(self):
219 """Return repr(self)."""
220 return self.to_str().__repr__()
223class QueryString:
224 """
225 Class for validated QueryStrings that can be used in api clients
226 """
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
234 @classmethod
235 def __check_arguments(cls, qs):
236 """
237 Check arguments on consistency
239 Args:
240 qs: queny statement object
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
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
264 Args:
265 qs: Query statement to add to the string object
267 Returns:
268 None
269 """
270 qs = self.__check_arguments(qs=qs)
271 self._qs.extend(qs)
272 self._qs = list(dict.fromkeys(qs))
274 def remove(self, qs: Union[Tuple, QueryStatement, List[QueryStatement]]):
275 """
276 Remove Statement from QueryString
277 Args:
278 qs:
280 Returns:
282 """
283 qs = self.__check_arguments(qs=qs)
284 for q in qs:
285 self._qs.remove(q)
287 @classmethod
288 def __get_validators__(cls):
289 yield cls.validate
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!")
300 def to_str(self):
301 """
302 Parsing self.qs to string object
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])
309 @classmethod
310 def parse_str(cls, string: str):
311 """
312 Creates QueryString from string
314 Args:
315 string:
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)
327 def __str__(self):
328 """Return str(self)."""
329 return self.to_str()
331 def __repr__(self):
332 """Return repr(self)."""
333 return self.to_str().__repr__()